ufront / ufront-mvc

The core MVC framework that powers ufront
MIT License
17 stars 15 forks source link

Proposal: Client side JS actions #30

Closed jasononeil closed 8 years ago

jasononeil commented 9 years ago

This is a fairly major idea, though it would be fairly easy to implement. I'd like to hear if anyone has feedback first before I start coding though :) I've tried to explain it in detail, with examples.

Background:

Ufront-Client is great at emulating a server-side HTTP request / response cycle entirely client side, in Javascript. But there are some actions that are just plain JS, and don't need a full page load, their own URL, or their own ActionResult / HttpResult.

I'm proposing we build a UFClientAction type to work for these situations.

Examples:

@:autoBuild(macro_to_make_sure_it_only_exists_on_client)
interface UFClientAction {
    public function execute( httpContext:HttpContext, ?data:{} ):Void;
}

class ClientJsApplication {
    /** The current HttpContext. Updated each time a new request happens. **/
    var currentContext:HttpContext(get,null);

    /** Run our usual Ufront requests as they do already. **/
    public function listen();

    /**
      Execute a given action with the current HttpContext.
      I might also let you provide the class name as a String here.
    **/
    public function executeAction( action:Class<UFClientAction>, ?data:{} );

    /**
      This might also be useful, for not re-triggering an action only on fresh-page-loads.
      So you could avoid initialising a timer on every pushstate link click, etc.
    **/
    public function executeActionOnce( action, ?data );

    /**
      A static helper function to trigger an action on the current app.
      We need to expose this so that JS (from the server, or from non-haxe code) is able to trigger an arbitrary action.
      I thought about using DOM events instead, but it would be harder to support old browsers without adding dependencies.
    **/
    @:expose("ufExecuteAction")
    public static function ufExecuteAction( actionName:String, ?data:{} );
}

Triggering actions from the client

From your Haxe code:

clientJsApp.executeAction( UpdateNotifications, { interval:30, onLoad:true } );

Or some arbitrary JS:

$("#btn").click(function() { ufExecuteAction("app.actions.DoTheThing"); });

We could possibly also have some tie in to HTML events:

<button onclick="ufExecuteAction('app.actions.DoTheThing')" />
<a href="/fallback/if/no/js/" data-uf-action="app.actions.DoIfJsActionsEnabled" />

How actions are added from a regular request (client or server)

We create a helper, like this:

import ufront.MVC;
import app.actions.InitParalaxScrolling;
using ufront.web.result.AddClientAction;

class HomeController extends Controller {
    @:route("/")
    public function home() {
        var vr = new ViewResult({ title: "Home" });
        return vr.addClientAction( InitParalaxScrolling );
    }
}

Similar to CallJavascriptResult, this would add a snippet to the response:

<script type="text/javascript">
ufExecuteAction( "app.actions.InitParalaxScrolling" );
</script>

Adding actions to your ClientJsApplication

Similar to how every UFApi is automatically imported into your app and available for injection, I propose we do:

var clientApp = new ClientJsApplication({
    actions: CompileTime.getAllClasses( UFClientAction );
});
// Which then does:
for ( action in actions ) {
    clientApp.injector.mapRuntimeValueOf( action ).toSingleton( action );
}

How actions are executed

When the app goes to execute an action:

jasononeil commented 9 years ago

This may be a solution to #23

kevinresol commented 9 years ago

Let me have trial on this concept:

// psuedo-code
class UpdatePasswordAction implements UFClientAction
{
  @inject public var asyncApi:MyAsyncApi;
  public function execute(ctx, data)
  {
    ui.showLoading(); // add a css spin wheel, disable the textbox etc
    asyncApi.updatePassword(data.password).handle(function()
    {
      ui.hideLoading(); // remove the stuff
    });
  }
}

is it that simple? That looks very nice to me!

kevinresol commented 9 years ago

One more thing, since this is purely client JS stuff. I anticipate a lot of DOM manipulation or HTML templating. I think adding some helper functionality would help, e.g:

// fetch (and cache) a template file from server and execute it with data
// then it can be used in: dom.innerHTML = resultHtml;
function executeTemplate(filePath:String, data:Dynamic)
francescoagati commented 9 years ago

i think that .addClientAction should be implemented also in PartialResult

francescoagati commented 9 years ago

Some other ideas . Passing params to action In partialresult the action si called after rendering of partial. calling an action first of rendering?

jasononeil commented 9 years ago

@kevinresol you have the right idea, glad you like it. I agree a bunch of helpers would be necessary. I recently cleaned up ViewResult, and it shouldn't be too hard to add a renderPartial(file,data):String helper now.

@francescoagati .addClientAction() would work with any ActionResult that returns HTML (so ViewResult, PartialViewResult, ContentResult etc). I'm not sure I understood the other ideas you were writing - could you explain a bit more?

jasononeil commented 9 years ago

One question to ask, is data type safe?

public function execute(ctx:HttpContext, ?data:Dynamic):Void
// or
public function execute(ctx:HttpContext, ?data:T):Void

Because it can be called via JS, or the version on the client may be different to the version on the server, we can't really guarantee type safety. So we can have no type safety or some type safety, but not fully guaranteed type safety.

Another option could be to use a macro to slot in a var data = Std.instance( data, T ) line, so the data is either correct, or null.

Also I'm thinking whatever T is should be JSON compatible, so it's easy to transport from server->client. We could also use Haxe serialization, but it makes it harder to interact with non-haxe code then.

francescoagati commented 9 years ago

Client Action can be generic and passing parameters is necessary sometimes. For example pass a reference of node element.

kevinresol commented 9 years ago

I prefer a type-safe one. For pure JS call, I think we can't do much checking there, for example T could be a structrual typedef which can't be checked at runtime.

what if UFClientAction declares a checkInput(data:T):Bool interface then use macro to force call it at the beginning of execute? so the user just implement their own method. They can choose to simply return true if they don't want to check it.

postite commented 9 years ago

cool ! that reminds me the signal/command used in mmvc ( based on robotlegs ) https://github.com/massiveinteractive/mmvc which i like a lot. i agree with the fact we should be able to interact with the action chains without having to rely on the dom. Also , you should have a look on how signal/command are called and executed in mmvc .I think this is a good example of what you are thinking of.