Jump to content

Let's bring hooks to JavaScript!


bernhard
 Share

Recommended Posts

What do you think of something like this?

// somewhere

let HelloWorld = ProcessWire.hookable(
  {
    foo: "foo world!",
    ___hello: () => {
      return "hello world!";
    },
  },
  "HelloWorld"
);

console.log(HelloWorld.hello()); // output: hello universe!
console.log(HelloWorld.foo); // output: foo universe!

// somewhere else

ProcessWire.addHookAfter("HelloWorld::hello", (e) => {
  e.return = "hello universe";
});

ProcessWire.addHookAfter("HelloWorld::foo", (e) => {
  e.return = "foo universe!";
});

I built a proof of concept module here, but it's already outdated and I'll work on a new version shortly.

The reason why I'm building this is because I needed a way to modify some parts of my JS frontend setup on my RockCommerce project. So I added two methods RockCommerce.after(...) and RockCommerce.before(...) where we as a developer and user of RockCommerce customise every part of the Frontend (for example adding custom taxes or pricing calculations).

Now while working on RockCalendar I wanted something similar. And while working on RockGrid I had the same need. For example adding custom locales or adding custom callbacks. What is easy as cake in PHP is not so easy to do in JS.

So I thought, wouldn't it be nice to have hooks in JS as well? I asked Ryan and he seems to like the idea, so I'll work on that over the next few weeks.

As you can see here I will most likely change the module soon to work with JS proxy objects. This is what provides the magic of AlpineJS behind the scenes.

Now I'm wondering if some basic Alpine magic would make sense here as well, for example watching a property and firing a callback can be extremely helpful on its own!

let demo = ProcessWire.magic({
  foo: "foo",

  init() {
    this.$watch("foo", (newVal, oldVal) => {
      console.log(`foo property changed from ${oldVal} to ${newVal}`);
    });
  },
});
demo.foo = "bar"; // will log "foo property changed from foo to bar"

I've renamed ProcessWire.hookable() to .magic() in this example, as it does more than just adding hook support. Not sure about "magic", maybe you have better ideas?

What do you think? Would that be helpful? What would be a good name? ProcessWire.rock(...) would be my favourite I guess 😄 ProcessWire.proxy(...) would also be an option.

Not sure if adding basic apline magic to PW would be a good idea of if adding alpinejs itself would be better?

Any JS experts here that want to help with that project?

  • Like 4
Link to comment
Share on other sites

@poljpocket thx for your question. It sounds like you totally misunderstood the idea of my proposal, so maybe the explanation was not good. I'll try it in other words:

Imagine you are building a module that stores monetary values in the database. They are stored as numbers (eg "123.45") in the db, but you likely want to present them to the user in a different way.

Now there could be hundreds of different formats the enduser might want to see and that would mean a lot of options for you as the module developer to support.

In RockCommerce for example I need the flexibility to modify prices based on the users need (eg customers/website visitors could see different prices based on their region and tax settings. See https://www.hetzner.com/storage/storage-box/ for example where you can choose the tax setting in the header and then you get different prices.

My proposal is to bring hooks to the JS world of ProcessWire as well, as we all know how powerful and useful these hooks can be!

In the example you as a module dev would only have to implement a hookable "___format()" method instead of the "format()" method without underscores and you'd only have to instantiate the class via ProcessWire.proxy(...):

class MyMoneyModule {
  ___format(num) {
    return "€ " + num;
  }
}

let demo = ProcessWire.proxy(new MyMoneyModule());

// this will output € 123.45
console.log(demo.format(123.45));

ProcessWire.addHookAfter("MyMoneyModule::format", (event) => {
  event.return = "Hooked format: " + event.return;
});

// this will output "Hooked format: € 123.45"
console.log(demo.format(123.45));

ProcessWire.addHookBefore("MyMoneyModule::format", (event) => {
  event.return = "****";
  event.replace = true;
});

// this will output "SOLD OUT"
console.log(demo.format(123.45));

I hope this makes things clear 🙂 

Link to comment
Share on other sites

No, I think I got you the first time :) But thanks for the clarification. Please, don't feel like I hate your proposal, I just think that with a watertight architecture, most of the logic in your examples should happen on the backend anyway. So, let me adapt my question to your example to show what I mean:

Why not fetch() the "sold out" state from the backend? The backend already knows if an item is sold out or not because the backend must validate the order anyway and thus must have it's own concept of deciding if an item can be sold or not.

So I as a module dev have to add a hook in JS to replace the price label in the frontend and additionally in the backend, block an order with a sold-out item in it with another hook in PHP.

Even if we simplify your example down to just the currency format you would like to have customizable: What about your PDF invoice generator in the backend? Again, the backend needs to know the currency format and thus, there is no need to duplicate the hooks in JS for the frontend.

I am sure there are examples where your proposal makes a lot of sense, like your second example which only changes the presentation of the data. For such things, I guess you should have a look at how the people over at Snipcart are doing their frontend customization, I really love their approach: https://docs.snipcart.com/v3/setup/customization

Link to comment
Share on other sites

26 minutes ago, poljpocket said:

Please, don't feel like I hate your proposal

Didn't feel like that, just thought you misunderstood the request from what you wrote.

26 minutes ago, poljpocket said:

I just think that with a watertight architecture, most of the logic in your examples should happen on the backend anyway.

It's really not about the backend. It has nothing to do with the backend. That's why I still don't understand your answers and your example using fetch() as that's a totally different story. My "sold out" wording was misleading in that regard, I'll edit that to avoid future confusion, thx.

26 minutes ago, poljpocket said:

Even if we simplify your example down to just the currency format you would like to have customizable: What about your PDF invoice generator in the backend?

Nothing. It really doesn't matter. It's absolutely not related to the backend at all. It's just about having the convenience and power of applying the same concept that we have in the backend for stuff on the JS side (most likely the pw admin area).

26 minutes ago, poljpocket said:

For such things, I guess you should have a look at how the people over at Snipcart are doing their frontend customization, I really love their approach: https://docs.snipcart.com/v3/setup/customization

You mean customization via dom element attributes? That might work for their use case but is something very different than what I propose. A very powerful concept of hooks are before hooks with event.replace = true. I don't think that is possible with the snipcart way. But I might be wrong. Admittedly I took just a quick look 🙂 

(edit: need a real world example why before hooks are super useful? see here)

As an example where dom-attributes make absolutely no sense or are totally limited: My module RockGrid uses tabulator.info to render tables. Rows are basically PW pages. So the backend only provides the endpoint to get a json with page objects aka table rows. Everything else happens on the frontend and from within JS. I only have a single dom element <div id='RockGrid-123'></div>

 

With hooks my clients could modify any part of the grid just by hooking into the relevant method. A lot can be done with callbacks and tabulator has an internal event bus that can be used for that, but I think hooks are a lot more powerful and feel a lot more familiar when you know pw well.

I might come up with better examples. Thx for your input.

Link to comment
Share on other sites

Now I feel bad because I thought I got you, but didn't. It's not about the frontend public website but about the edit experience in the backend PW admin area. My bad and I stand corrected: For that, the approach indeed makes a lot of sense!

Edited by poljpocket
reflect wording changes from Bernhard
Link to comment
Share on other sites

Haha you don't have to. I appreciate the input and questions. It shows I have to be more careful with the explanations. The terms "frontend" and "backend" are very confusing in that context as they refer to two things. Backend could either be the pw admin area or the server implementation, in our case php. Frontend could be the websites public frontend or it could be the JS implementation on the frontend (public website) or the backend (pw admin).

I try to be more careful with the wording in my explanations once I have something working. At the moment I'm fighting with modifying arguments from within the addHookBefore...

Link to comment
Share on other sites

Ok got it working! I think that's quite cool and has a lot of potential. Does that code example make sense @poljpocket ?

class HelloWorld {
  ___greet(salut = "hello", what = "world") {
    return `${salut} ${what}`;
  }
}

const helloWorld = ProcessWire.wire(new HelloWorld());

// shows hello world
console.log(helloWorld.greet());

// shows hi there
console.log(helloWorld.greet("hi", "there"));

// add BEFORE hook
ProcessWire.addHookBefore("HelloWorld::greet", (event) => {
  event.arguments(0, "hallo");
  event.arguments(1, "welt");
});

// shows hallo welt
console.log(helloWorld.greet());

// shows hallo welt
console.log(helloWorld.greet("servas", "oida"));

// add AFTER hook
ProcessWire.addHookAfter("HelloWorld::greet", (event) => {
  console.log(event.arguments());
  event.return = "hi universe";
});

// shows ['hallo', 'welt']
// shows hi universe
console.log(helloWorld.greet());

And another one showing hook priority:

class PrioDemo {
  ___greet() {
    return "hello world";
  }
}

const prio = ProcessWire.wire(new PrioDemo());

ProcessWire.addHookAfter(
  "PrioDemo::greet",
  () => {
    console.log("second");
  },
  20
);
ProcessWire.addHookAfter(
  "PrioDemo::greet",
  () => {
    console.log("first");
  },
  10
);
ProcessWire.addHookAfter(
  "PrioDemo::greet",
  () => {
    console.log("third");
  },
  30
);

// shows
// first
// second
// third
// hello world
console.log(prio.greet());

 

Link to comment
Share on other sites

So is this a proposed solution as a template for module developers to implement in any JS-enabled modules, is it for PW to implement in its JS-powered backend implementations, or even something else entirely...? I'm just wondering how or where the integration of this would take place.

I can see the benefit to this being offered, whether or not I can immediately see a need for myself at this point in time.

  • Like 1
Link to comment
Share on other sites

  • 1 month later...
  • 8 months later...
  • 5 weeks later...

I guess I’m really confused here. I’m good at PHP and at databases and at about ten other development languages, but in my (gak!) 45 years of code development, I have thus far managed to avoid JavaScript nearly completely — so it’s entirely possible that I’m blinkered here in a way that I don’t understand at all.

But in a client-server world, JavaScript is basically a way to control the user’s browser, right? To get it to do specific things based on user actions and preferences that are better done locally and not by the server? So ... when would I actually want such a hook on the server? Would it be called by the client’s browser? How/why/when would it do so?

Or is this JavaScript hook being called by my PHP code on the server? If so, ... why on earth would I want that instead of a PHP hook?

Honestly, I’m not trolling or being willfully ignorant. If this is something that would actually be useful, I truly want to understand it and support you.

OTOH, if it’s useful only to a few module developers — and only to make things a bit simpler for them, not to add functionality that can help me make things more pleasant or powerful for users — then I’d prefer that Ryan’s resources not be spent making sure your work harmonizes properly with core code.

I’ve read twice through this thread, and also the write-up in today’s ProcessWire Weekly, but I don’t feel like I’m understanding this at all. (Possibly, the edits that @bernhard and @poljpocket have made in their early posts to correct their own misunderstandings of each other actually have also made the discussion harder to follow. But more likely I’m just being dense.)

Thanks for any clarification you can bring to this for me!

Link to comment
Share on other sites

JavaScript hooks are totally client side. There is no Server involved and also no PHP. Everything happens in JavaScript. The reason why they are extremely useful is because the developer of the JS module can easily make things customisable and we as devs or users can use hooks to change the code execution.

Imagine someone builds a modal component for the core. We have a modal.show() and modal.hide() method.

Without hooks, that's it. Maybe the developer was wise and added events to it that we can listen to. Maybe something like "modal:shown" or "modal:hidden". UIkit for example has these events:

beforeshow Fires before an item is shown.
show Fires after an item is shown.
shown Fires after the item's show animation has been completed.
beforehide Fires before an item is hidden.
hide Fires after an item's hide animation has started.
hidden Fires after an item is hidden.

Now the problem: How would you, as a developer, modify the behaviour of the modal? For example how would you prevent showing the modal under certain circumstances?

That's easy when having hooks. All we need is making ___show() and ___hide() hookable and we don't need those events any more, because we can hook into the modal's business logic:

  • ProcessWire.addHookBefore('Modal::show')
  • ProcessWire.addHookAfter('Modal::show')
  • ProcessWire.addHookBefore('Modal::hide')
  • ProcessWire.addHookAfter('Modal::hide')

--------------

Another example is RockCommerce. The challenge here is not the backend - we all have the same backend. The challenge is the frontend, as every frontend is different. And every RC customer might need different pricing calculations or different features (like preventing to select items, max amount, custom tax calculations, coupons, etc...). All this can easily be done by providing hooks. RockCommerce does the heavy lifting and provides the base implementation and my customers can add modifications as they need.

--------------

Another example could be a DatePicker. It often has some init call like this:

const picker = new WhateverDatePicker({
  firstWeekday: 0,
  okLabel: "Apply",
  ...
});

This code will be somewhere in a core .js file. That means it is hardcoded! How would we make the "firstWeekday" setting customisable by the user? So that some can set Monday and some Sunday as first day of the week? And what about the okLabel? etc, etc...

With hooks it is easy. The dev just adds a hookable method and all good:

Class WhateverDatePicker {
  function init() {
    const picker = new WhateverDatePicker(this.getSettings());
  }
  
  ___getSettings() {
    return {
      firstWeekday: 0,
      okLabel: "Apply",
      ...
    }
  }
}

Now anybody could easily hook into the setup of the Datepicker:

ProcessWire.addHookAfter('WhateverDatePicker::getSettings', (event) => {
  let settings = event.return;
  settings.firstWeekday: 1; // change from 0 to 1
  settings.okLabel: 'Anwenden';
});

 

Link to comment
Share on other sites

On 8/9/2025 at 12:52 AM, ErikMH said:

I guess I’m really confused here. I’m good at PHP and at databases and at about ten other development languages, but in my (gak!) 45 years of code development, I have thus far managed to avoid JavaScript nearly completely — so it’s entirely possible that I’m blinkered here in a way that I don’t understand at all.

Hi Erik and thanks for your comments. I think @bernhard got the advantages of such systems across pretty well in his own response. I also was very confused at first.

The main reason @bernhard proposed this was to allow users of his modules (who are also developers) can extend the functionality of the modules he offers. Since the concept is cool and for that purpose also makes a lot of sense, he proposed this to be included in the core.

And I think, this is where the main confusion for all of us comes from – it is the question why this needs to be in the core in the first place: It only needs to be part of the core if the core also makes use of it. Which means that most of the JavaScript code in the core needs to be overhauled to allow meaningful places to hook into.

At this time, though, I am less and less sure if this is really needed. The current module offered I think is perfectly fine: Developers can install the module if needed or leave it be if it's not needed. We might go as far as to create a JavaScript library on NPM or similar platforms for it to be included directly in the code base of any module wanting to use it.

Edited by poljpocket
Link to comment
Share on other sites

  

Quick shoutout and thanks to @bernhard for both RockCalendar and RockJavaScriptHooks.

Modifying the module UI to fit client preferences was one line in some JS added to the admin. It really opens up a lot of opportunities for making module JS as flexible and powerful as ProcessWire. Nice work 🙌

I came to post this randomly and didn't know about the ongoing conversation. Might have been good timing.

@ErikMH Here is an example on site I'm working on. By default, RockCalendar times are 24hr, this hooks into the JavaScript method that creates the timepicker and changes it to 12hr time. JS hooks are supported in RockCalendar.

// Set calendar time input to 12 hour
ProcessWire.addHookAfter('RockCalendarPicker::settings', e => e.return.timePicker24Hour = false);

Without this ability my alternative is to modify the module where it would break later or request a feature. "Can you add a config option for 12hr time" is a feature request I didn't have to write and @bernhard didn't have to read/implement and his module has even more utility right off the shelf. In cases where you're working on an open source/free module that can be a really big deal as well.

Just to contribute a little more to the greater conversation- there is a lot of value in this. I would go as far to say "it will become clear how useful it is when you really need it." Hypotheticals for hooks are difficult. Most of the hooks in PW core are just "neat" until the time comes when they solve an issue or let you do something really cool.

On 8/11/2025 at 12:51 AM, poljpocket said:

Developers can install the module if needed or leave it be if it's not needed.

This would be more true if there were true dependency support in the core the way that it exists in Composer. Until then the requirement of dependencies is difficult to put on the user unless they come from the same author or are somehow packaged together in a way that handles that more easily. This is especially true since there is segmentation by module type (Inputfield, Fieldtype, Process, Markup, etc) and manually installing these one b one becomes exponentially tedious.

If dependency installation existed then there could be fewer feature requests for Ryan or core contributors and that development can remain focused on the "framework" part of ProcessWire rather than individual unrelated feature requests. Perhaps this would be a good feature request for the core.

In this case, JS hooks are a natural application of a fundamental feature on the client side implemented in a syntactically consistent way which encourages adoption and potential utility in the same. That would be my argument for core over module.

On 8/11/2025 at 12:51 AM, poljpocket said:

Which means that most of the JavaScript code in the core needs to be overhauled to allow meaningful places to hook into.

Core support for the ability to hook into JS scripts can be established beforehand so that modules can take advantage of it and the core can implement gradually. It is an enhancement, not a breaking change, and doesn't modify the core API. A rollout where updates are prioritized by the greatest utility when feasible is an acceptable path forward IMHO.

If hooks were part of the core I would write them into modules I build. The value of doing that is future creativity and problem solving by developers who use them.

  • Like 1
Link to comment
Share on other sites

Thanks for the example, though which perfectly illustrates my points about why this doesn't need to be in the core if the core doesn't use it:

RockCalendar has first-class support for JavaScript hooks. I can't show you the code but we can see it in the Changelog:

image.png.ac228f4c2f8238df4a7562b9b59a7eb8.png

This means, that your hooked method "RockCalendarPicker::settings" actively has been written as "RockCalendarPicker::___settings" by the author of the module themselves. Likely, they also added a second change to the code of the module, possibly by adding the single file right here to the code base.

Right now, other than using the global variable "ProcessWire", it doesn't have anything to do with ProcessWire at all. It doesn't interact with anything in the core. The module is already loading it's own scripts supporting hooks, likely including the hooks boilerplate part already too. So what exactly do we need the proposed core change for in this situation? It's important to remember the context which is to assume there are no changes to the core's JavaScript code.

Now, try to do the same adjustment for the ProcessWire core's datepicker. You can't, not even with the module in question here installed. Not even if you go to the file /wire/modules/Inputfield/InputfieldDatetime/InputfieldDatetime.js and prefix every method with "___". You'll need to completely redesign the code you find in the file to support hooks like how you are demonstrating. And by the way, you don't even need to, because you can change InputfieldDatetime's time format on the server side with hooks or by reconfiguring it.

Don't get me wrong: @bernhard's solution is very good and very neat and he's using it in his modules already which I love. But this also demonstrates perfectly that there is no need for a core change in order for this to work in any module outside of core. As I already pointed out multiple times, supporting hooks in the core's JavaScript means that most of the code has to be completely overhauled.

Edited by poljpocket
Link to comment
Share on other sites

@poljpocket "It's not in the app so it shouldn't be in the app" isn't really a good argument against anything in software.

12 hours ago, poljpocket said:

The module is already loading it's own scripts supporting hooks, likely including the hooks boilerplate part already too. So what exactly do we need the proposed core change for in this situation?

Because it's a feature that would be beneficial to module developers and future utility. I think that's the point here- is the case being made for utility. I can see situations where this would be useful in the future and I don't have a reliable way to create dependencies that would make building on top of this functionality easy for me or users. Let's call my argument entirely separate from @bernhard as a developer who would love to see this supported without a complex module requirement process that doesn't exist and would require a lot more work to implement.

12 hours ago, poljpocket said:

Now, try to do the same adjustment for the ProcessWire core's datepicker. You can't, not even with the module in question here installed. Not even if you go to the file /wire/modules/Inputfield/InputfieldDatetime/InputfieldDatetime.js and prefix every method with "___". You'll need to completely redesign the code you find in the file to support hooks like how you are demonstrating. And by the way, you don't even need to, because you can change InputfieldDatetime's time format on the server side with hooks or by reconfiguring it.

If/when the time comes that something would benefit from adding hooks to the module, then it would be a good time to do it. There is no need to go and edit all of the JS for each module just to make it immediately available. It is perfectly fine to say that "many existing modules do not yet support JS hooks, but they may in the future". But the InputfieldDatetime.js example is probably a good example of where one may be easier to convert, at some point down the line (if it ever needed it), since it is a relatively simple JS file with few methods and not many lines of code at all.

An alternative way of looking at this is that should the next version of a Datetime picker be chosen to replace this one in the core, for any number of reasons, then that would be a good time to write it for the first time. If a third party module for an alternate Datetime picker was to be written, it could make use of JS hooks if it's useful or valuable.

 

All that aside, here's something that may resonate with the wider PW developer community. The server side API for ProcessWire is wildly powerful and flexible, however the client side is an area that has a lot of opportunities for growth. It doesn't have the dynamic nature of more modern front-ends. ProcessWire is built on jQuery and one of the things that @bernhard mentions were events. jQuery event listeners do respond to vanilla JS dispatched events and vanilla JS events can't be heard by jQuery. This can create interoperability issues for modules that primarily use vanilla JS and modules/core that strictly use jQuery. This bit me in the last couple of months. Hooks are an opportunity to bridge that gap, in the future. I need to stress this point- nobody expects the entire core to support this immediately or asking for it to. 

ProcessWire does not use event-driven architecture, it uses hooks to provide similar functionality. JS hooks are an opportunity to provide some sort of parity and additional powers on the front end that could translate somewhat into "if you know server-side ProcessWire hooks, you'll be comfortable using client-side hooks".

One of the things that is really difficult for me to work around is not having a more dynamic UI in the admin. Having core and third party modules JS call hooks on specific events would make this much, much easier without having to write a lot of code or build an entirely new select input module. This is the first step towards an overarching client side admin API, which doesn't currently exist.

Here's my hot take- jQuery is popular and easy to use but it will become a legacy library and in many ways it already is. I haven't used jQuery in projects in many, many years and the most recent lines I had to write were adding duplicate events fired in jQuery that I was already dispatching in vanilla JS in a module to make the rest of the UI see changes to fields. There are a lot of devs who really don't want to write jQuery. In 2025 it isn't necessary for jQuery to exist other than existing dependency or personal syntactical preference. There are going to be an increasing amount of people who want to use ProcessWire that have never written one line of jQuery.

Does anyone expect the core to fully support hooks in all modules now? Tomorrow? Next week? Not at all. But having a framework in place that allows the core to continue using jQuery without overhauling it to bring a lot of extended functionality is really valuable.

Nobody is asking that ProcessWire be rewritten in Alpine or htmx or something, hooks are a consideration for introducing more powerful features in the style of ProcessWire as part of a roadmap.

12 hours ago, poljpocket said:

supporting hooks in the core's JavaScript means that most of the code has to be completely overhauled.

Here are my questions @poljpocket:

What is the best way to introduce new client side powers without completely overhauling the core JavaScript?

Why is introducing powerful utilities that developers can use as part of ProcessWire being a framework, with parity in approach between server/client apis, that it may not prioritize using itself bad?

If hooks can be introduced incrementally as needed, "as convenient", or by contributions and PRs- is that really completely overhauling the core JS?

It seems like you are shooting down the simple examples, which are simple to show how easy using them can be, but haven't made the case for a true net negative other than saying "it will require work".

On 8/11/2025 at 12:51 AM, poljpocket said:

It only needs to be part of the core if the core also makes use of it.

This isn't how frameworks work, but it also doesn't preclude the value of having it available for use by the core in the future.

  • Thanks 1
Link to comment
Share on other sites

Wouldn‘t it be possible to add this as a module dependency to my own modules if JS-Hooks would be a released module on the PW module store? Maybe this would be the first step for other module authors to try out the module and to see if others would adopt using it. 

Link to comment
Share on other sites

I have been called out for just talking and not actually doing some work repeatedly. So I followed up on @bernhard's proposal, simplified it, added a JS build system and added some example hooks for InputfieldDatetime to illustrate what this can do. It's still a draft because at this time, I am not sure if I did understand the hooks system correctly especially when dealing with callbacks.

https://github.com/processwire/processwire/pull/323

What do you think? Sorry Bernhard that I removed your rockdevtools integration and example code. I am trying to get as close to a mergeable implementation as possible.

  • Like 1
Link to comment
Share on other sites

11 hours ago, poljpocket said:

I am trying to get as close to a mergeable implementation as possible.

Would you mind sharing your sources for what makes code mergeable?

Link to comment
Share on other sites

What I mean by mergeable is that if Ryan wants, he can click "Merge Pull Request". To facilitate this, I removed the examples (which would be added to the docs or as PHPdoc) and the reference to RockDevTools since there can't be a reference to a non-core module.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...