faylang / fay-angular

Fay FFI bindings for AngularJS
BSD 3-Clause "New" or "Revised" License
24 stars 2 forks source link

Generating controllers via ffi templating #4

Open kfish opened 10 years ago

kfish commented 10 years ago

In commit 2956a4ba (branch example-templating) I'm trying to generate Angular controller code via Fay ffi,

In src/Angular.hs there is:

data StateController a = SC
  { muts :: [(a -> a)]
  , gets :: [Text]
  }

stateController :: StateController a -> a -> Fay ()
stateController = ffi "\
  \ (function($scope) { \
    \ $scope.state = %2; \
    \ var sc = %1; \
    \ \
...

which is called in examples/todo/todo.js:

TodoCtrl = Strict.Angular.stateController(Strict.TodoFay.todoSC,Strict.TodoFay.initialState);

However this fails with:

Error: Argument 'TodoCtrl' is not a function, got Fay$$Monad

The goal is to generate all the Angular code required for a Controller (and later, for a Directive etc.) from a Haskell specification, so fay-angular would need to generate various javascript classes/functions that are structured in the required way, with argument name $scope etc. Any suggestions about where the best place to implement this would be? is it possible from within a fay library, or will it require some modification to the fay compiler itself?

bergmark commented 10 years ago

This might be an omission in the runtime for serializing the fay monad, i'll look into it!

As a side note, calling the FFI from the strictness wrapper, pretty funky stuff :) But I think it'll work.

bergmark commented 10 years ago

I added that test case, but I also tested this one which doesn't work, it may be the same issue:

module StrictWrapper where

clog2 :: Int -> Double -> Fay ()
clog2 = ffi "(function () { console.log(%1, %2); })()"

main :: Fay ()
main = (ffi "Strict.StrictWrapper.clog2(234)" :: Double -> Fay ()) 345

It might be the same as faylang/fay#267 too.

Could you try making a smaller reproduction of this please?

kfish commented 10 years ago

The following 3 files are intended to generate a minimal valid angular controller that is initialized via a function call. It fails wtih

Error: Argument 'ConstCtrl' is not a function, got Fay$$Monad

AngularConst.hs (build with "fay --package fay-text AngularConst.hs --strict AngularConst"):

module AngularConst where

import FFI
import Prelude

constController :: Int -> Fay ()
constController = ffi "\
  \ (function($scope) { \
    \ $scope.val = %1; \
    \ console.log($scope.val); \
    \ $scope.getVal = function () { return $scope.val }; \
  \ })"

const.js:

ConstCtrl = Strict.AngularConst.constController(7);

index.html:

<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
    <script src="AngularConst.js"></script>
    <script src="const.js"></script>
  </head>
  <body>
    <h2>Constant</h2>
    <div ng-controller="ConstCtrl">
      <span>Initialized with {{val}}, returns {{getVal()}}</span>
    </div>
  </body>
</html>

The generated code is:

AngularConst.constController = function($p1){
  return new Fay$$$(function(){
    return new Fay$$Monad(Fay$$jsToFay(["unknown"], (function($scope) {  $scope.val = Fay$$fayToJs_int($p1);  console.log($scope.val);  $scope.getVal = function () { return $scope.val };  })));
  });
};

By comparison, a working javascript version of const.js is:

mkConstController = function (val) {
  return (function ($scope) {
    $scope.val = val;
    console.log($scope.val);
    $scope.getVal = function () { return $scope.val };
  })
}

ConstCtrl = mkConstController(7);
bergmark commented 10 years ago

Try the second commit, it seems to do the trick. I have the same fay module as you:

module AngularConst where

import           FFI
import           Prelude

constController :: Int -> Fay ()
constController = ffi "\
  \ (function($scope) { \
    \ $scope.val = %1; \
    \ console.log($scope.val); \
    \ $scope.getVal = function () { return $scope.val }; \
  \ })"

This html:

<!doctype html>
<html>
  <head>
    <script src="AngularConst.js"></script>
    <script>
var c = Strict.AngularConst.constController(7);
var o = {};
var r = c(o);
    </script>
  </head>
  <body>
  </body>
</html>

Compiling with fay --pretty --Wall --strict AngularConst --library AngularConst.hs --runtime-path js/runtime.js (--runtime-path is just so you don't have to recompile fay when you modify runtime.js)

This logs 7, o.val => 7, o.getVal() => 7

kfish commented 10 years ago

Yes, that does load without errors, and if I modify the generated constController to log when it is called, it gets called.

When loaded with Angular though, the generated code does not seem to be recognized as an Angular controller. The generated code is:

> console.log(ConstCtrl)
function (){
      var fayFunc = fayObj;
      var return_type = args[args.length-1];
      var len = args.length;
      // If some arguments.
      if (len > 1) {
        // Apply to all the arguments.
        fayFunc = Fay$$_(fayFunc,true);
        // TODO: Perhaps we should throw an error when JS
        // passes more arguments than Haskell accepts.

        // Unserialize the JS values to Fay for the Fay callback.
        if (args == "automatic_function")
        {
          for (var i = 0; i < arguments.length; i++) {
            fayFunc = Fay$$fayToJs(["automatic"], Fay$$_(fayFunc(Fay$$jsToFay(["automatic"],arguments[i])),true));
          }
          return fayFunc;
        }

        for (var i = 0, len = len; i < len - 1 && fayFunc instanceof Function; i++) {
          fayFunc = Fay$$_(fayFunc(Fay$$jsToFay(args[i],arguments[i])),true);
        }
        // Finally, serialize the Fay return value back to JS.
        var return_base = return_type[0];
        var return_args = return_type[1];
        // If it's a monadic return value, get the value instead.
        if(return_base == "action") {
          return Fay$$fayToJs(return_args[0],fayFunc.value);
        }
        // Otherwise just serialize the value direct.
        else {
          return Fay$$fayToJs(return_type,fayFunc);
        }
      } else {
        throw new Error("Nullary function?");
      }
    } 

This doesn't look like the function($scope) {..} that Angular is expecting. Also I see no mention of the captured 7, but I don't really understand what is going on :)

bergmark commented 10 years ago

That's the jsToFay code for functions, it partially applies the arguments it gets to the original fay function you are serializing. It's supposed to be called as a normal function. Can you tell why angular doesn't accept it?

bergmark commented 10 years ago

c is the same thing in my example by the way. The runtime has to do this wrapping since it doesn't know how many arguments the fay function expects.

kfish commented 10 years ago

My guess is that Angular expects a function argument called exactly "$scope"; if I modify a javascript Angular controller and rename the $scope argument, Angular fails to load the controller.

@btford is this the case, or do you have any suggestions about how we should interface with Angular?

bergmark commented 10 years ago

Ehm, i'm not sure what to think of this :) http://stackoverflow.com/a/12108723/182603

bergmark commented 10 years ago

Oh, I forgot about this detail:

Strict.AngularConst.constController(7)()
=>
function ($scope) {  $scope.val = Fay$$fayToJs_int($p1);  console.log($scope.val);  $scope.getVal = function () { return $scope.val };  } 

though i think this won't work in general. I have to think more about serializing actions before I know what the implementation should really be.

btford commented 10 years ago

@kfish Angular infers the name based on the function parameters, unless you add annotations. This is convenient when developing, because it frees you from having to maintain two lists of dependencies for a controller and keep them in sync. However, you probably want to avoid that in a case like this. :)

function MyCtrl ($scope) {
  // ...
}

Is the same as:

function MyCtrl (foo) {
  // $scope aliased to foo
}.$inject = ['$scope'];

More info here: http://docs.angularjs.org/guide/di#dependency-injection_dependency-annotation_-annotation

ibotty commented 10 years ago

My guess is that Angular expects a function argument called exactly\ "$scope"

that's exactly right. that can be mitigated when using explicit dependency injection. see "dependency annotation" in http://docs.angularjs.org/guide/di for two ways to pass them as strings (which fay should not mangle and which are nice for minification as well).

bergmark commented 10 years ago

Thanks for the input! Happy to see that there are other ways of doing it :)

I'm not sure if the other approaches will make much difference, what we have are functions generated dynamically by fay so we can't force structure on it (as in naming arguments or assigning properties to them). But like I said, I need to experiment some more with the serialization to see what it should actually look like.

@kfish you can use what i wrote above for now at least.

kfish commented 10 years ago

@bergmark wow yes, just adding an extra () in const.js works; the serialized function is generated, including the verbatim "$scope" as needed. I'll try to modify the StateController similarly ...

@btford @ibotty thanks for your advice!

bergmark commented 10 years ago

I thought a bit more about it, this only works here because we have the function as an ffi string. I'm not sure what your plans are, but if you try to create these controllers from normal fay code you will end up with unnamed functions and this won't work. We'd need to be able to tell angular "yes this is really a controller" somehow, or wrap the fay action through js or the ffi before passing it to angular.