multiple actions `onclick` (run sequentially, including re-running a cell)?

I have data stored in AWS S3 and I would like change & save these data, then re-load the saved object so that updated data are reflected back into my notebook. To trigger this event, I have a button.

According to a discussion on StackOverflow, I should be able to trigger multiple button events on click as such:

onclick="${change_data};${reload_object};" // not my real cell names, but indicative of the task

When I try this, however, the notebook doesn’t seem to re-load my data, and the addition of the second action seems to break the button’s functionality (that is, the button works as expected with the first parameter only, but doesn’t do anything with the addition of the second parameter).

Playing around, I think part of the issue that I’m facing is that trying to re-run a cell using the buttononly ‘sorta’ produces my desired effect: the button click will run the content a cell as written, but will not re-run the cell itself.

I would appreciate any pointers on how to accomplish the following:

  1. link two actions to a button click
  2. ensure that the second action always executes after the first
  3. re-run a cell as part of the a button action (to refresh stored data)

Also problematic: Running my ‘load data’ cell also triggers Observable into re-evaluating and re-running my ‘create new record’ cell, which has the effect of adding a new record each time data are loaded. :confused: [Yikes! Always so many hurdles.]

Here’s my notebook to better inform this question:

Any and all guidance and insight are welcome and appreciated!

1 Like

I find the hypertext literal is simpler for attaching onclick event handlers (https://observablehq.com/@observablehq/htl)

it replaces the standard html<stuff> with a better one.

For handlers I refactor them to their enclosing scope like so:

mything = {
    function click(evt) {
         // You are in normal JS land. Do as many things as you want
    }
    return html`<button onclick=${click}>reload record</button>`
}

For state like s3 contents which is outside the Observable reactive paradigm. I place it in a mutable. Maybe with an unfullfilled promised so it does not ‘fire’ initially.

mutable s3contents = new Promise(() ={})

then my click handler would be writing a value to that

onClick() {
// read s3
mutable s3contents = …
}

And operations on the contents would cascade from the s3contents variable. If the intitial value is a promise I don’t need to handle the initial case.

1 Like

A lot to unpack here. :slight_smile:

Important distinction: You don’t “trigger multiple events”, but perform multiple actions when the event is triggered. The click event is triggered once, when the button is clicked.

As a rule of thumb, don’t ever use the onclick attribute (or any other on* attributes) in Observable. Here you’re injecting the string representation (xyz.toString()) of change_data and reload_object, and as a consequence lose all variable references. Also, in this case you’re not executing the functions; (${change_data})() would execute them (but again, not recommended).

Remember that you’re not required to put all logic into a single line. Use Observable’s block statements and attach your handlers via the DOM element’s API:

viewof my_button = {
  const button = html`<button>Click me!`;
  button.addEventListener('click', do_one_thing);
  button.addEventListener('click', do_another_thing);
  // Alternatively via the on* property:
  button.onclick = function() {
    do_one_thing();
    do_another_thing();
  };
  return button;
}

Alternatively you can make your actions depend on your button via the Runtime:

// The undefined value prevents dependent cells from running before the first click.
viewof my_button = Object.defineProperty(html`<button>Click me!`, 'value', {value: undefined})
{
   my_button; // create a dependency via Observable's Runtime.
   do_one_thing();
   do_another_thing();
}
1 Like

Thanks for the guidance and feedback, Tom and Fabian!

I am still far from achieving what I initially estimated would be simple. Here it is in components:

Triggering an Event

Regrettably, I am not quite achieving even re-running my load data function with any of the possible approaches you describe (to the extent that I understand them). I’ve updated the notebook with each of the 3 button approaches, which is super helpful to be aware of!] [EDIT: Linking to view of the notebook at point when these changes were shown; subsequently cut.]

Here’s what I mean about re-running the load data cell:

All (non-recommended and recommended :slight_smile: thanks!) button constructs fail to re-load the s3 object in the same way as re-running/executing that that cell. This perplexes me, as the cell itself works when run, and elsewhere (where I write back to S3) my buttons all seem to run the functions just fine.

Separating Actions

As shown above (assuming my GIF loads) when I have a ‘write to S3’ cell defined that concatenates a new array to an object of arrays downloaded from S3, re-running the ‘load data’ cell triggers an (unintentional) side-effect of adding a new array into my source data, since the Observable runtime is aware of the inter-connection between these cells .

I haven’t succeeded with dropping mutable s3contents = new Promise(() ={}) into my notebook and then randomly messing around. Earlier I had tried messing around with `await` no to avail. It’s time for me to hunker down and learn more methodically. I have the Introduction to Promises, Mutable Values, and Introduction to Mutable State open now and will work through them.

Template Literals and ‘String’ Data

It wasn’t evident to me that one needed to change data to strings when passing it between Observable and S3, and I can’t really say that I fully get it. This was once of the missing pieces in working with S3 via Observable that kept me from making a connection previously.

The hypertext literal looks powerful (great documentation there!). I must admit that I still haven’t fully realized how to use it correctly here. I appreciate that you referred me back to it and will keep reading. It would be terrific if it would allow me to somehow get past the ‘stringify’ step.!

Take a closer look at these cells:

{
   button_alternative_3; // create a dependency via Observable's Runtime.
   loadObject;
}
loadObject = s3.getObject(objectParams).promise()

What do you expect to happen here, and why do you think it should work this way? :slight_smile:

And please try to keep your answer short and focused on this question alone - let’s tackle one issue at a time.

1 Like

Is the issue the promise?

What I expect is for the loadObject function to pull down the object from S3 (as it does when I manually re-run that cell; sorry, GIF showing this initially didn’t work; now restored above). I need to re-run this after I inject new data into the S3 object elsewhere, but this only appears to work when i manually trigger the cell. [Overall, I am trying to use Observable as a means of editing and saving back data to S3.]

The .promise component in this script is necessary to get over a callback issue that I encountered trying to make getObject work in the browser.

Is it correct that the button is working, but doesn’t appear to effect a change because the operation is a promise? If so, what differentiates this from this one?

create_new_array = s3.putObject({
        Body: new_array_formatted,
        Bucket: bucketName,
        Key: objectName
      }).promise()

…which does appear to work with a button:

html`<button onclick=${create_new_array}>create new record</button>
  • Please let’s stay focused on these two cells, because understanding what went wrong here might help you solve your problems throughout the rest of the notebook.
  • Please also understand that the following questions are meant to steer you in finding the correct answer, so please answer them as directly as possible.
  • If you cannot answer a question immediately, try to research the correct answer first before answering.
loadObject = s3.getObject(objectParams).promise()

When will this cell run? Will it ever run again? If you think the answer is “yes”, can you explain why?

{
   button_alternative_3; // create a dependency via Observable's Runtime.
   loadObject;
}

What effect is placing loadObject; in this cell supposed to have? In what directions do cell dependencies work in Observable?


I realize that you’d probably prefer a straight answer over this slightly annoying questionaire, but I believe that you’ll profit more in the long term if you are able to discover these answers yourself.

To the contrary! I appreciate it and thank you for your time! I’ll reply more in a minute after trying to answer. :wink:

As written, this cell will run on page load.

I thought it would re-run if I triggered it as an action from a button.

(Thinking about this while trying to ignore the statement “because my other button seems to work” :wink: ) In each of the three ways that you demonstrated to construct a button, there are event handlers that triggers an action. In my case, that action (I assumed) was to re-run the loadObject cell.

Placing loadObject; in this cell separates the loading of the object from the clicking of the button, such that clicking the button triggers the cell, but triggering the cell doesn’t result in the button being re-run.

Observable runs the cells in topological order: “If cell B needs the value of cell A , Observable won’t run cell B until A is computed. Likewise whenever the value of cell A changes, cell B is automatically recomputed.”

I very much get this and appreciate it. Trying to answer specifically/directly here. Regrettably not quite there yet: I remain perplexed as to the difference between manually running the loadObject cell versus trying to ‘trigger’ it as an action from elsewhere.

It sounds to me like you’re applying certain patterns without fully understanding how or why they work. As a result you’re missing the nuanced differences that cause them to fail. Try to compare them side by side to spot changes in syntax.

You’ve quoted the description, but I’d rather hear it in your own words, applied to the given example. Try to graph the dependencies/relationship between your loadObject cell, your button cell, and the intermediate cell where you reference both.

Focus on this statement, then try to figure out which conditions cause the loadObject cell to be recomputed. Try to verify your assumption by reproducing a simplified example in a separate notebook.

I suspect that you’re misattributing cause and effect. Try to remove/disable some of your cells. Remember to reload your notebook in order to test those cells which will only run initially, but may have a lingering effect.

I think I am seeing it a bit better now (not that I have a solution yet):

The button clicks are performing the action defined in loadObject, but that doesn’t mean anything for the loadObject cell itself (i.e. they are calling down their own data object, but that action is not reflecting back into the loadObject` cell.) Effectively, this means that the loadObject cell isn’t getting the new data.

(And I just caught your prompts above; thank you! I’ll continue trying to get my head around this… first my partner is asking that I take out the garbage and attend to my weekend chores :wink: )

Thank you for your time and gentle guidance!

Your observations are totally accurate, @mootari: I have discovered how to do things over time but really am not well versed with what is happening or why. As is probably evident from your couple of years of interaction with me (thank you!), I tend to have a problem that I wish to solve, I research solutions and monkey around till I have something that works, and often drill down from working examples into specifics as I try to modify those patterns for other purposes.

Thank you for teaching me these different patterns. I am trying to learn and appreciate the nuances and am making progress. In this case, binding the data loading to the button click as follows ensures that my code re-executes each click and updated data is loaded:

loaded_data_value = {
   button_alternative_3; // create a dependency via Observable's Runtime.
   var value = s3.getObject(objectParams).promise();
   return value;
}

(After I stop procrastinating on my weekend home chores by obsessing over this) I’ll try to re-work the notebook more learning from this step forward to see if I can construct something that works as I intend and will report back.

Thank you!!!

OK: I’ve ascertained that while building out each function separately helps me to learn and to confirm that steps are working, I will have to combine actions within separate operations to keep them from interfering with one another.

I can create an ‘add record’ event as follows, pulling down the latest copy of the data at the moment that I intend to write to it:

viewof update_object = Object.defineProperty(html`<button>Add Record`, 'value', {value: ""})

-coupled with-

update_as_dependency = {
   update_object; // create a dependency via Observable's Runtime.
  var source_object = await s3.getObject(objectParams).promise();
  var data_object_formatted = JSON.parse(source_object.Body.toString('utf-8'));
  var new_array = Array.from(data_object_formatted).concat(bibliography_update_template[0]);
  var new_array_formatted = JSON.stringify(await new_array);
  var create_new_array = s3.putObject({
        Body: new_array_formatted,
        Bucket: bucketName,
        Key: objectName
      }).promise();
  return create_new_array
}

What I haven’t yet managed to do is to link my 'load data; action with my 'update ’ action, such that writing a new record will trigger a fresh of the data loaded into the notebook. But getting closer! Got it! :partying_face::tada:

I can use the dependency structure that you showed me to ensure that data are re-loaded when I click an ‘add record’ cell:

data_object = {update_object;
var source_object = await s3.getObject(objectParams).promise();
var source_object_formatted = JSON.parse(source_object.Body.toString('utf-8'));
return source_object_formatted
                  }

Thanks for all the help and guidance!

My next step will be to figure out how to have this data refresh cell listen to more than one button, as I intend to extend the application to enable a sort of editor interface (the ‘add record’ function being my hacky solution to adding a new row of data, that I could subsequently edit).

Here’s another link to the notebook (just in case anyone wishes to reference without scrolling up to the start of the thread):

Thanks again!!!

1 Like