batchPlay; can I call a repetative function that is OUTSIDE the .addEventListener?

I want to move the “repetativeActions()” function outside the addEventListener because I have many buttons with almost the same function.

WORKING…

const batchPlay = require("photoshop").action.batchPlay;

document
.getElementById("Test Button")
.addEventListener('click', async () => {

// Button uniqueActions();

  let renameLayer = "rename layer";
  repetativeActions();

    async function repetativeActions () {
      await batchPlay([

      //AN EXAMPLE
      // Select front layer
      {"_obj":"select","_target":[{"_enum":"ordinal","_ref":"layer","_value":"front"}],"makeVisible":false},
  
      // Make 2 layers
      {"_obj":"make","_target":[{"_ref":"layer"}],"layerID":14,"using":{"_obj":"layer","color":{"_enum":"color","_value":"red"},"name":"Action make new layer"}},
      {"_obj":"make","_target":[{"_ref":"layer"}],"layerID":14,"using":{"_obj":"layer","color":{"_enum":"color","_value":"red"},"name":"_pfff"}},

      // Rename last layer
       {"_obj":"set","_target":[{"_enum":"ordinal","_ref":"layer","_value":"targetEnum"}],"to":{"_obj":"layer","name":renameLayer}}

      ],{})

      };
      async function runModalFunction() {
        await require("photoshop").core.executeAsModal(repetativeActions, {"commandName": "Action Commands"});
      }
      
      await runModalFunction();
      
});


I want to move the entire repetativeActions() function outside to be called and used by many buttons…
Any help is appreciated.

You could move the function, or you could change the event listener to a class shared by all your buttons instead.
That said, I think you should have another look at your function as currently `repetativeActions’ is calling itself recursively.

Almost! You’re getting the undefined error because your function doesn’t return anything.

return await batchPlay([
  // batchPlay action descriptors...
)]

This will fail though as your batchPlay needs to be wrapped in an executeAsModal function because it changes the document state and so PS needs to be in a modal state.

// Import executeAsModal 
const {core} = require("Photoshop");

// Change batchPlay call to:
return await core.executeAsModal(()=> {
  return await batchPlay([
    // Batchplay descriptors...
})

That said, you only need to return a value if you need it, and it doesn’t look to me like to me if you would in this situation, so you could not worry about it, refactor your function to use executeAsModal, and change your function call in you event listener to just call the function rather than assign it to a variable:

document
.getElementById("TestButton40")
.addEventListener('click', async () => {
  await repetativeActions();
});

On a side note; you reassign result in your event listeners, but it’s declared inside repetativeActions() which means it’s block scoped to that function and as such it isn’t available to you outside that function.
You could either forget about it as detailed above, otherwise you could declare the variable outside the function (and before the function is called). That’s more suited for when you want to update the variable from within the function, so in this instance I wouldn’t bother.

As for the other option of listening to a class rather than an id:

const sharedFunctionBtns = document.querySelectorAll(".className");
sharedFunctionBtns.forEach((btn) => {
  btn.addEventListener("click", functionToBeCalled);
}

You’ll still need to make the changes to your repetativeActions function though.

ok, Thank you so very much !
It’s now much more elegant when the code is written properly.

Thank you very much for your very helpful interaction. Before, I really didn’t understand + I think somethings have changed since API version 1 and 2 concerning batchPlay.

Can I assume that because these event listeners are asynchronous, that they will complete in top-down order no matter the complexity of the batchPlay commands? ( beginning, unique to each button, end)

Here is my working batchPlay example;

const batchPlay = require(“photoshop”).action.batchPlay;

//-------------------
// CLASS=; Repetative BEGINNING actions BEFORE button click

const precursorActions = document.querySelectorAll(“.sp-tab-page”);
precursorActions.forEach( (btn) => {btn.addEventListener(“click”, repetativePrecursorActions);

async function repetativePrecursorActions(){
await require(“photoshop”).core.executeAsModal( async () => {
let newLayerName = “Begin with precursor actions”
//ASSUME 3 LAYERS MINUMUM
await batchPlay([
//Select backmost layer
{“_obj”:“select”,“_target”:[{“_enum”:“ordinal”,“_ref”:“layer”,“_value”:“back”}],“makeVisible”:false},
//Rename layer
{“_obj”:“set”,“_target”:[{“_enum”:“ordinal”,“_ref”:“layer”,“_value”:“targetEnum”}],“to”:{“_obj”:“layer”,“name”:newLayerName}}
], {});
});
};
});

// ID= BUTTON CLICK actions unique for each button identified
document
.getElementById(“TestButton40”)
.addEventListener(‘click’,async () => {

buttonUniqueActions();

async function buttonUniqueActions(){
await require("photoshop").core.executeAsModal( async () => {  
      let newLayerName = "Eureka! - Unique Actions" 
      //ASSUME 3 LAYERS MINUMUM
      await batchPlay([
       //Select backmost layer  
       {"_obj":"select","_target":[{"_enum":"ordinal","_ref":"layer","_value":"back"}],"makeVisible":false},
      //Select forward layer
      {"_obj":"select","_target":[{"_enum":"ordinal","_ref":"layer","_value":"forwardEnum"}],"makeVisible":false},
      //Rename layer
      {"_obj":"set","_target":[{"_enum":"ordinal","_ref":"layer","_value":"targetEnum"}],"to":{"_obj":"layer","name":newLayerName}}
      ], {});
    });
}});

//Repeat for each unique button
//    :
//    :
// CLASS= Repetative ENDING actions AFTER button click

const ancillaryActions = document.querySelectorAll(".sp-tab-page");
ancillaryActions.forEach(  (btn) => {btn.addEventListener("click", repetativeAncillaryActions);

 async function repetativeAncillaryActions(){
 await require("photoshop").core.executeAsModal( async () => {  
  let newLayerName = "End with Ancillary actions" 
       //ASSUME 3 LAYERS MINUMUM
       await batchPlay([
       //Select backmost layer  
       {"_obj":"select","_target":[{"_enum":"ordinal","_ref":"layer","_value":"back"}],"makeVisible":false},
       //Select forward layer
       {"_obj":"select","_target":[{"_enum":"ordinal","_ref":"layer","_value":"forwardEnum"}],"makeVisible":false},
       //Select forward layer
       {"_obj":"select","_target":[{"_enum":"ordinal","_ref":"layer","_value":"forwardEnum"}],"makeVisible":false},
       //Rename layer
       {"_obj":"set","_target":[{"_enum":"ordinal","_ref":"layer","_value":"targetEnum"}],"to":{"_obj":"layer","name":newLayerName}}
       ], {});
  });

Glad to help!
I’m not sure I understand your question - are you expecting each function to run in sequence from a single click? If so, then no, they’ll only trigger when the event listener for each is triggered.
If you’re asking if the array of action descriptors in each function will execute in sequence, then yes, but not because they’re in async functions - they do because batchPlay iterates through the array. Making the functions async means that the JavaScript engine waits until that function completes before moving on to the next line of code (provided you await that is).

It appears to be working like I expect it should, but I don’t know if I implemented good coding practices.

There are many buttons on a tab page,
Every button has class=.sp-tab-page.
Every button has a unique id.

So, you helped me figure out when a button is clicked…

  1. Code a single Class event listener array. Any “click” event with Class= “.sp-tab-page”, which is every button on a sp-tab-page, will batchPlay precursor or beginning actions…
  2. Code many different event listeners for each button id to batchPlay unique action commands
  3. Repeat #1 with another single Class event listener array, This time, after all the unique button id’s — and you have got an ending batchPlay action.

RECAP. So, for every button click;

  1. Global Beginning actions are batchPlayed
  2. Unique actions for each button are batchPlayed
  3. Global Ending actions are batchPlayed.

Through fault or design, It appears to be working!

Back to my question;
Will #1,2,3 always perform 1 then 2, then 3, because of the asyn/ wait assignments as I have coded?

TIA

Ok, I understand you now!
Firstly , eventListener sequence, this is getting a bit beyond my JS knowledge but as I understand it this is determined by the browser/client. Modern browsers go in ascending order, but some older browsers go in reverse (IE9 or Netscape). I don’t know about UXP, but based on your experience it sounds like ascending order.
There’s more to it than just that, and is perhaps a bit too much to go into here, but I suggest you go and have a look at event capture and bubbling.
Anyway, that’s not actually that relevant as I think there’s a better approach to your use case.
Rather than having three event listeners for what is essentially a single process is a bit messy, let’s wrap it all up in one event listener that dynamically changes functionality based on the button id.
The key to this is that batchPlay takes an array of action descriptors as it’s first argument and by using spread operators we can construct that array by combining other arrays into one.

// Index.html
<sp-button id="btn1" class="btn">Btn 1</sp-button>
<sp-button id="btn2" class="btn">Btn 2</sp-button>
<sp-button id="btn3" class="btn">Btn 3</sp-button>

// Index.js
// Imports
const { core } = require("photoshop");
const batchPlay = require("photoshop").action.batchPlay;

// Define generic action descriptor arrays that are the same for every button click. 
// Each array is returned from a function so that newLayerName can be passed as an argument when the function is called and applied appropriately to the array.
const preActions = (newLayerName) => {
  return [
    {
      "_obj": "select",
      "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "back" }],
      "makeVisible": false,
    },
    {
      "_obj": "set",
      "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "targetEnum" }],
      "to": { "_obj": "layer", "name": newLayerName },
    },
  ];
};

const postActions = (newLayerName) => {
  return [
    {
      "_obj": "select",
      "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "back" }],
      "makeVisible": false,
    },
    {
      "_obj": "select",
      "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "forwardEnum" }],
      "makeVisible": false,
    },
    {
      "_obj": "select",
      "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "forwardEnum" }],
      "makeVisible": false,
    },
    {
      "_obj": "set",
      "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "targetEnum" }],
      "to": { "_obj": "layer", "name": newLayerName },
    },
  ];
};

// Define context specific actions. Again, returned from a function so that a variable can be passed in as an argument and applied to the array/s. 
// This is a bit more complicated than the previous two; what we are returning here is an array of objects, each representing the set of actions to be performed for a specific button. 
// Note that each object has two properties: an id (which corresponds to the id of a button), and an array of action descriptors. 
// When we call this function later we will filter the results for the entry with the id that matches the id of the button clicked.
// In this example they all have the same action descriptors and only the id varies, and like your example assumes at least three layers are present in the active document.
const contextSpecificActions = (newLayerName) => {
  return [
    {
      "id": "btn1",
      "actions": [
        {
          "_obj": "select",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "back" }],
          "makeVisible": false,
        },
        {
          "_obj": "select",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "forwardEnum" }],
          "makeVisible": false,
        },
        {
          "_obj": "set",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "targetEnum" }],
          "to": { "_obj": "layer", "name": newLayerName },
        },
      ],
    },
    {
      "id": "btn2",
      "actions": [
        {
          "_obj": "select",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "back" }],
          "makeVisible": false,
        },
        {
          "_obj": "select",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "forwardEnum" }],
          "makeVisible": false,
        },
        {
          "_obj": "set",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "targetEnum" }],
          "to": { "_obj": "layer", "name": newLayerName },
        },
      ],
    },
    {
      "id": "btn3",
      "actions": [
        {
          "_obj": "select",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "back" }],
          "makeVisible": false,
        },
        {
          "_obj": "select",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "forwardEnum" }],
          "makeVisible": false,
        },
        {
          "_obj": "set",
          "_target": [{ "_enum": "ordinal", "_ref": "layer", "_value": "targetEnum" }],
          "to": { "_obj": "layer", "name": newLayerName },
        },
      ],
    },
  ];
};

// Get all button elements
const buttons = document.querySelectorAll(".btn");

// Add event listener to each button - we do this in a for...of loop as it allows asynchronous code within (forEach does not)
for (const btn of buttons) {
  // Make event listener async so we can await executeAsModal
  btn.addEventListener("click", async (event) => {
    // Get the id of the button clicked from the event. You could even just make this const btnId = event.target.id
    const target = event.target;
    const btnId = target.id;

    // Within the batchPlay call we construct the array of action descriptors using spread operators. 
    // We call each of our functions that return action descriptor arrays, passing in the newLayerName value. 
    // In the case of the contextSpecificActions call we use array.find() to get the entry that matches the clicked button id, and then .actions to just get the array of action descriptors from the returned object.
    // The end result is a single array, unique to the button clicked.
    await core.executeAsModal(async () => {
      return batchPlay(
        [
          ...preActions("Pre Actions"),
          ...contextSpecificActions("Contextual Actions").find((x) => x.id === btnId).actions,
          ...postActions("Post Actions"),
        ],
        {}
      );
    });
  });
}

I think you’ll agree that this is a cleaner approach which is also much more scalable - adding a new set of contextual actions is as easy as adding an entry to the array and popping a new button in the UI.
In regards to your questions about asychronous code; you’ll note that the only async code is for the executeAsModal function call - this is so that the JS engine waits until batchPlay returns before moving on to any subsequent code (which in this example there is none). It has no bearing on the sequence in which the action descriptors are run, that is defined by their order in the array passed to batchPlay.

Yes!
Yes!
I wasn’t happy with the “grabbing 3 straws” approach in my code - even though it worked. This new structure is just excellent.

This is starting to make sense to me. Your explanations are right on.

While batchPlay is great for development, I know I’m going to have to port it later. But having a good code structure will allow me to do that much more easily.

Thank you again. I’ve got some knowledge to create many things now.

1 Like

No worries, glad to have helped - being self-taught I’m not sure I’d know how to code if it wasn’t for the kindness of strangers on the internet giving me their time!

Thanks! I try and give detail in my answers that helps people understand the code rather than just copy/paste it so it’s nice to hear that!
Asynchronous JS is notoriously hard to grasp, but once it clicks you’ll kick yourself over how simple it actually is!

I’m not sure I agree with you about porting batchPlay later, whilst initially it may not be as readable as using API methods, good practice would be to wrap action descriptors up into helper functions as above, but also abstracting the executeAsModal/batchPlay call part of the process into another helper function, and essentially building your own set of API methods. Here’s a rough pseudocode example:

// A helper function to run batchPlay commands 
const runBatchPlay = async (descriptors) => {
  return await core.executeAsModal(() => {
    return batchPlay(
      descriptors,
      {}
    );
  });
}

// A helper function to define a set of action descriptors
// e.g open file, duplicate layer, transform, etc
const someActionDescriptors = () => {
  return [
    // array of descriptors
  ]
}

// An obviously named function to be used within main function for readability's sake
const rotateLayer180Deg = async () => {
  await runBatchPlay(someActionDescriptors)
}

// Main function - we run our program in an async function so we can await within
const main = async () => {
  await rotateLayer180Deg();
}

// Run main function - no await because a) not allowed in top-level code as not wrapped in async function b) there's no code to follow it so it doesn't need to await
main();

This could be handled in other ways (e.g. using a class) but this is probably the easiest to understand.
The main benefit of batchPlay is that it’s as “bare metal” as you can get and as such faster for UXP to execute. It also gives you full access to PS, which the API will most likely take a while to reach parity (if ever). Worth noting that a regular comment from the experienced UXP devs on this forum is “I only use batchPlay”!