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

Just to understand better:

For your use case, is there any specific reason not to use:

  • fetch() to load the config from an API, or
  • $config->js() or $config->jsConfig() to hydrate your JS from the backend, or
  • inline JS as config in the <head>?
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...

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...