hydrostack / hydro

Hydro brings stateful and reactive components to ASP.NET Core without writing JavaScript
https://usehydro.dev
MIT License
720 stars 17 forks source link

Execute JS from Hydro actions #75

Closed kjeske closed 2 months ago

kjeske commented 3 months ago

Ability to queue JS statements from Hydro actions.

taublast commented 3 months ago

On it, will attack splitting latest feats into separate PRs..

kjeske commented 3 months ago

How far are you in separating this one? In the tutorial I'm working on I need one fix that you had in here (reapplying the scripts on updating), so I wondered if you are soon done, or I should fix the script tag issue separately?

taublast commented 3 months ago

Hey, sorry for the delay, hitting some hot deadlines with several projects so, you can imagine.. :)

After tuning Hydro, now consuming it, can't say enough how awesome it is. Like blazor without blazor, some kind of god-mode cheat for MVC razor views. :)

The main problem for me is time, many features have gotten mixed in my fork, and it became an untrivial task to separate them all one from another, when one is dependent from another. So would need more time for that, as the whole fork went much forward. I hope I can attack them by the end of the month.

Can propose a faster alternative: I could make a list of all the features I see actually in the fork, and You could point out those that do not go along with the main concept (like that form post feature, that I needed badly to fast port existing views to hydro). Then I could create one pr with all of them them excluded, that seems to be a fast task that I could do quickly in one evening.

taublast commented 3 months ago

..At the same time, if you need just this feature ASAP, I guess I could do a quick PR by excluding everything except this one. Should we do it?

kjeske commented 3 months ago

hitting some hot deadlines with several projects so, you can imagine.. :)

Oh yes, good luck then!

After tuning Hydro, now consuming it, can't say enough how awesome it is.

Nice to hear! :) And thank you for all the great contributions!

The main problem for me is time, many features have gotten mixed in my fork, and it became an untrivial task to separate them all one from another, when one is dependent from another.

Maybe you could create a new PR for this issue with all the changes you have? Then I can extract only the ones for 'Execute JS' and review them at the same time. Does it sound ok?

taublast commented 3 months ago

https://github.com/hydrostack/hydro/pull/78

kjeske commented 3 months ago

Thank you!

taublast commented 2 months ago

There is an issue to be solved, to order an js execution after the location call.

Example:

  Location(url);
  ExecuteJs("myCode()");

if no [SkipOutput] is present the component will update, inject/execute myCode() from headers and then will apply new Location logic. Meanwhile we expect the script to be executed after the location change. Any ideas on how to best implement this?

taublast commented 2 months ago

Fixed this in my fork.

How to use, example:

  [SkipOutput]
  public void OpenCallMe()
  {
      string url = Url.Action("Index", "Home");
      Location(url);
      ExecuteJs("goToCallForm()");
  }

What is happening here:

  1. [SkipOutput] is preventing the script execution before the Location has executed. Without it this component would first update and the script would be executed before the Location logic.
  2. The Location logic will be executed, hydro.js would scroll to top and only after that our js goToCallForm() will be executed (in this example by scrolling where now want, anchors unrelated).

Works fine, overall what changed is we are now passing the component Id along with the script body in headers to be able to inject script to the corresponding element and we are processing script headers after processing Hydro-Location too.

Changes:

https://github.com/taublast/hydro/commit/31052ca9f63106eb823b21922b91429e025a9daf

kjeske commented 2 months ago

In such use cases I would suggest to use the payload with indication to execute some logic on the next page. Then this behaviour could be controlled by C# payload objects instead of JS code on each page when you want to apply this logic. It would limit the JS code in components to minimum, which is aligned with Hydro philosophy 😄

Example:

public record HomePayload(bool ScrollToCallForm);
[SkipOutput]
public void OpenCallMe()
{
    Location(Url.Action("Index", "Home"), new HomePayload(ScrollToCallForm: true));
}
public class CallForm : HydroComponent
{
    public override void Mount()
    {
        var payload = GetPayload<HomePayload>();

        if (payload != null && payload.ScrollToCallForm)
        {
            ExecuteJs("goToCallForm()");
        }
    }
}

Btw since this is the current component's DOM element, we could use something like:

ExecuteJs("this.scrollIntoView()");

Of course it would work only if you have the CallForm as a Hydro component 😄

Would that work for you?

kjeske commented 2 months ago

Since in Hydro views we already have a way to execute JS using handlers, I used the same syntax for the Hydro actions to keep it consistent:

public void Test()
{
    Client.Invoke("console.log('test')");
}

But maybe I will just change it in both places to Client.ExecuteJs so it's more clear what we are calling

taublast commented 2 months ago

I seem i bit stuck with a following problem:

Normally if we update a component and pass JS along with headers this JS is injected into this component: ...appendChild(script); and gets executed by browser.

This breaks when the script execution is ordered from a closing dialog, or similar component that is about to get destroyed.

The script is injected fine but then the browser doesn't have time to execute it as the following tick removes the component from dom, along with the injected script.

If I just debug then the browser has time to execute it, to demonstrate the cause of the issue.

If I inject the script from headers let's say to the end of the body tag, then everything works fine. But then we would end up with a body growing in size with every script added there. We could in theory clean up the body injected scripts tracking the timestamp to remove old scripts but that looks like a dirty move.

Thoughts?

kjeske commented 2 months ago

That's a good point and it makes sense to append scripts on the body. I just experimented a bit with some ideas and this is what I ended up with in the createScriptTag function:

script.innerHTML = content;

if (autoRemove) {
  script.id = generateGuid();
  script.innerHTML += `setTimeout(() => document.getElementById("${script.id}").remove());`;
}

The script will remove itself after execution.

taublast commented 2 months ago

I'm just injecting at the end of hydro-body if any, looks good so far:

script.setAttribute('data-injected', 'true');
 let attachTo = document.getElementById('hydro-location');          
  if (attachTo) {
      attachTo.appendChild(script); // will get removed on next update anyway
  }
  else{
      //document.body.appendChild(script); <- would require to delete expired ones manually so... avoiding this
      componentElement.appendChild(script);
  }
kjeske commented 2 months ago

I've already released this feature in v0.15.0 with auto-removal which seems to work great - you might want to take a look. In the next step I will be releasing Hydro JS events, and then submitting the whole form on submit.

taublast commented 2 months ago

Have you managed to include the feature to execute JavaScript embedded with views HTML?

kjeske commented 2 months ago

Yes, those scripts will be executed as well