danieleteti / delphimvcframework

DMVCFramework (for short) is a popular and powerful framework for WEB API in Delphi. Supports RESTful and JSON-RPC WEB APIs development.
Apache License 2.0
1.25k stars 360 forks source link

Helpers for HTMX #686

Closed fastbike closed 1 year ago

fastbike commented 1 year ago

I've been playing around with HTMX (https://htmx.org/) a light weight hypertext first web framework that uses server side rendering which reduces the amount of javascript code in an app by a large factor.

Other languages have helper classes for HTMX to work with HTTP request and response objects. I've written some code as class helpers for the DMVC TMVCWebRequest and TMVCWebResponse frameworks that encapsulate the server side aspects and wondering whether they could be added to the library ? Anyway here's the public interface, with some examples how they get used in the DMVC controller code.

(As an aside, porting this code over from scripting type languages gave me more appreciation for the strongly typed nature of Pascal - where I could declare types for the various arguments to make it easier for application writers, the php implementation had to check the spelling of the argument and then raised an error if it was not within the allowed values. Which complicates their code and makes it a second class experience for the application writer !)

  ///<summary>Helper class to expose HTMX headers as native functions on WebRequest objects</summary>
  THTMXRequestHelper = class helper for TMVCWebRequest
  public
    /// <summary>Indicates that the request is triggered by HTMX.</summary>
    function IsHTMX: Boolean;
    /// <summary>Indicates that the request is via an element using hx-boost.</summary>
    function IsBoosted: Boolean;
    /// <summary>True if the request is for history restoration after a miss in the local history cache</summary>
    function IsHistoryRestoreRequest: Boolean;
    /// <summary>The current URL of the browser.</summary>
    function GetCurrentUrl: string;
    /// <summary>The user response to an hx-prompt.</summary>
    function GetPrompt: string;
    /// <summary>The id of the target element if it exists.</summary>
    function GetTarget: string;
    /// <summary>The id of the triggered element if it exists.</summary>
    function GetTrigger: string;
    /// <summary>The name of the triggered element if it exists.</summary>
    function GetTriggerName: string;
    /// <summary>The value of the header is a JSON serialized</summary>
    /// <remarks>Requires the event-header extension to be installed and loaded on the page </remarks>
    /// <see>https://htmx.org/extensions/event-header/ </see>
    function GetTriggeringEvent: TArray<string>;
    /// <summary>The value of the header is a JSON serialized</summary>
    /// <remarks>Requires the event-header extension to be installed and loaded on the page </remarks>
    /// <see>https://htmx.org/extensions/event-header/ </see>
    function GetTriggeringEventAsJason: TJsonObject;
  end;

  ///<summary>Helper class to expose HTMX options as native functions on WebResponse objects</summary>
  THTMXResponseHelper = class helper for TMVCWebResponse
  public
    /// <summary> Pushes a new url into the browser history history.</summary>
    /// <remarks>This creates a new history entry, allowing navigation with the browser’s back and forward buttons.
    /// This is similar to the hx-push-url attribute. If present, this header overrides any behavior defined with attributes</remarks>
    /// <param name="URL">A URL to be pushed into the location bar.
    /// This may be relative or absolute, as per history.pushState().
    /// If omitted, the header will output "false", which prevents the browser’s history from being updated.</param>
    function SetPushUrl(URL: string = ''): TMVCWebResponse;
    /// <summary>Replaces the current URL in the browser location history.</summary>
    /// <remarks>This does not create a new history entry; in effect, it removes the previous current URL from the browser’s history.
    /// This is similar to the hx-replace-url attribute.
    /// If present, this header overrides any behavior defined with attributes.</remarks>
    /// <param name="URL">A URL to replace the current URL in the location bar.
    /// This may be relative or absolute, as per history.replaceState(), but must have the same origin as the current URL.
    /// If ommitted, the header will output "false", which prevents the browser’s current URL from being updated.</param>
    function SetReplaceUrl(URL: string = ''): TMVCWebResponse;
    /// <summary>Allows you to specify how the response will be swapped. See hx-swap for possible values</summary>
    /// Check if transition: true works ?
    function SetReswap(Option: TSwapOption): TMVCWebResponse; overload;
    /// <summary>Allows you to specify how the response will be swapped. See hx-swap for possible values</summary>
    /// <remarks>You can modify the timing of the browser update to synchronize htmx with the timing of CSS transition effects.</remarks>
    /// <param name="SwapDelay">The  amount of time that htmx will wait after receiving a response to swap the content (in seconds)</param>
    /// <param name="SettleDelay">The amount of time between the swap and the settle logic(in seconds)</param>
    function SetReswap(Option: TSwapOption; SwapDelay, SettleDelay: Integer): TMVCWebResponse; overload;
    /// <summary>Allows you to specify how the response will be swapped. See hx-swap for possible values</summary>
    /// <remarks>You can modify nature of the browser update to show or scroll to the top or bottom of a target.</remarks>
    /// <param name="Option">The target for the swap</param>
    /// <param name="ShowScroll">Whether to set the display to the target, or to scroll to the target</param>
    /// <param name="To">Either top or bottom</param>
    /// <param name="Selector">Allows targetting of a different element for scrolling or showing</param>
    function SetReswap(Option: TSwapOption; ShowScroll: TShowScrollType; &To: TSwapScrollTo; Selector: string = '')
      : TMVCWebResponse; overload;
    /// <summary>A CSS selector that updates the target of the content to a different element on the page</summary>
    function SetRetarget(Selector: string): TMVCWebResponse;
    /// <summary>Allows you to trigger a client side event.</summary>
    /// <remarks>Using events gives you a lot of flexibility to add functionality to normal htmx responses.</remarks>
    /// <param name="Name">The name of the javscript event to be triggered</param>
    /// <param name="After">The timing of the event</param>
    function TriggerClientEvent(Name: string; After: TClientEventType = etReceived): TMVCWebResponse; overload;
    /// <summary>Allows you to trigger a collection of client side events.</summary>
    /// <remarks>Using events gives you a lot of flexibility to add functionality to normal htmx responses.</remarks>
    /// <param name="Names">A collection of the names of the javscript events to be triggered</param>
    /// <param name="After">The timing of the event</param>
    function TriggerClientEvents(Names: TArray<string>; After: TClientEventType = etReceived): TMVCWebResponse;
    /// <summary>Allows you to trigger a client side event with parameters.</summary>
    /// <remarks>Using events gives you a lot of flexibility to add functionality to normal htmx responses.</remarks>
    /// <param name="Name">The name of the javscript event to be triggered</param>
    /// <param name="Params">An object containing the parameters to be sent to the event</param>
    /// <param name="After">The timing of the event</param>
    function TriggerClientEvent(Name: string; Params: TValue; After: TClientEventType = etReceived): TMVCWebResponse; overload;
    /// <summary>if set to “true” the client side will do a a full refresh of the page</summary>
    function SetPageRefresh(Refresh: Boolean = true): TMVCWebResponse;
    /// <summary>Allows you to do a client-side redirect that does not do a full page reload</summary>
    /// <remarks>Instead of changing the page’s location it will act like following a hx-boost link, creating a new history entry,
    /// issuing an ajax request to the value of the header and pushing the path into history. </remarks>
    function SetLocation(Path: string): TMVCWebResponse; overload;
    /// <summary>Used to do a client-side redirect to a new location</summary>
    function SetRedirect(Path: string): TMVCWebResponse;
    /// <summary>Sends an error response bcack to client.</summary>
    function SetErrorResponse(ErrorCode: Integer; ErrorMessage: string): TMVCWebResponse;
    /// <summary>A CSS selector that allows you to choose which part of the response is used to be swapped in.</summary>
    /// <remarks> Overrides an existing hx-select on the triggering element</remarks>
    /// <param name="Selector">A CSS selector </param>
    function SetReSelect(Selector: string): TMVCWebResponse;
  end;
fastbike commented 1 year ago

Some examples

  1. Inside of a controller, forces the html that is sent back to be scrolled into the active view
    // first param is an enum of types of substitution, second is to scroll or directly show, third is at top or bottom
    Context.Response.SetReswap(soInnerHTML,  ssScroll, sstBottom);
  2. Trigger a javascript event on the web page to be run after the http response is received
    Context.Response.TriggerClientEvents('myEvent');
    // trigger two events, or more) to be called sequentially
    Context.Response.TriggerClientEvents(['myEvent', 'myEvent2']);
  3. Trigger an event on the web page, passing it a native Delphi object, serialised as a json structure
    var
    Facility: TFacility;
    // code to create and populate the object goes here
    ...
    // send it to the web page
    Context.Response.TriggerClientEvent('savedEvent', Facility);
danieleteti commented 1 year ago

Nice. What you could do is to create a sample project which uses these helpers. Then we can put the sample in the official samples. This could be also a showcase about using HTMX with dmvcframework.

fastbike commented 1 year ago

Cool, I'll add it to my todo list :)

fastbike commented 1 year ago

I've created a simple CRUDS style app showing a list of movies, which shows how to incorporate HTMX into Delphi MVC Framework. Where shall I upload the zip file to ?

danieleteti commented 1 year ago

Hi, just send me the zip "d (dot) teti (at) bittime (dot) it" and I'll add it to the repo. Otherwise you can create a PR.

Thanks

DT

Il dom 3 set 2023, 03:14 David Moorhouse @.***> ha scritto:

I've created a simple CRUDS style app showing a list of movies, which shows how to incorporate HTMX into Delphi MVC Framework. Where shall I upload the zip file to ?

— Reply to this email directly, view it on GitHub https://github.com/danieleteti/delphimvcframework/issues/686#issuecomment-1703978588, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAK4ZJCQQICQ2QVMM5YGRD3XYPKY3ANCNFSM6AAAAAA4FA64HA . You are receiving this because you commented.Message ID: @.***>

fastbike commented 1 year ago

Pull request created

danieleteti commented 1 year ago

https://github.com/danieleteti/delphimvcframework/pull/690