Famous / famous-angular

Bring structure to your Famo.us apps with the power of AngularJS. Famo.us/Angular integrates seamlessly with existing Angular and Famo.us apps.
https://famo.us/angular
Mozilla Public License 2.0
1.92k stars 275 forks source link

feat: ability to access render nodes from the controller #238

Closed jamwise closed 9 years ago

jamwise commented 9 years ago

Background: It's great that we can use famo.us objects mostly the "angular way" using directives and services. But it would be equally great to be able to access the render nodes directly. Some applications of this might be if a developer wants to overwrite built in methods, access "un-pipe-able" events such as through the sync object on the famous scrollview, or to interact with a famo.us object strictly in javascript which might save some clutter in the markup in some cases.

Solution: A way to assign famo.us objects to a variable which will be accessed from the scope of a controller.

Obstacles:

  1. Currently the directives in famous-angular run after the controller so if the object were passed to the scope, it wouldn't be available at the execution of the controller code.
  2. Angular's isolate scope doesn't allow the creation of variables in the controller scope from the directive.

Proposed solution: Create a service/directive combo. The directive to name the famous render node, and a service to provide a helper method to access the objects "asynchronously". The service functionality could just be wrapped into the $famous service as there's no benefit to it being external.

The Directive

.directive('faObject', ['$famousDecorator', 'faObject', function ($famousDecorator, faObject) {
  return {
    compile: function() {
      return {
        post: function(scope, element, attrs) {
          var isolate = $famousDecorator.ensureIsolate(scope);
          faObject(attrs.faObject,isolate.renderNode);
        }
      };
    }
  };
}])

The Service

.factory('faObject', function($rootScope) {
  var faObjects = {};

  return function(name,objectOrMethod) {
    var type = (typeof objectOrMethod === "function") ? "function" : 
               (typeof objectOrMethod === "object") ? "object" : false;

    if(type === "function") {
      $rootScope.$watch(function(){
        return faObjects;
      },function(faObjects){
        objectOrMethod(faObjects[name]);
      });
    } else if(type === "object") {
      faObjects[name] = objectOrMethod;
    }

  };
});

In Use

Since the service listens for the change on the object, we'll have to pass it a callback instead of accessing methods directly on the object.

Using the directive

    <fa-scroll-view fa-pipe-from="scrollViewPipe" fa-object="scrollObject">

Using the service

    .controller('myController', function($scope, faObject) {

            faObject("scrollObject",function(scrollObject){

                scrollObject.sync.on("start",function(){
                    console.log("**Started Scrolling**");
                });

            });

    });
zackbrown commented 9 years ago

Hey @i11ume — this is a very interesting idea.

We do have a solution in place that I believe solves the background/solution piece ('A way to assign famo.us objects to a variable which will be accessed from the scope of a controller.') I'll explain it, and then please tell me if you think it doesn't address what you're proposing to solve here.

One of our original primary goals with the Famo.us/Angular integration was to fully support vanilla Famo.us as well as vanilla Angular. There should be no 'dead-ends' or aspects of either of the vanilla frameworks that you sacrifice by using this integration. So this piece about being able to, e.g. overwrite built in methods, access "un-pipe-able" events such as through the sync object on the famous scrollview, or to interact with a famo.us object strictly in javascript was something we wanted to handle out of the gate.

There are a lot of parallels between Famo.us and DOM. Both are trees, both can be created with HTML (thanks to Famo.us/Angular, in Famo.us's case) and both can be created/manipulated entirely through JavaScript (this is the default for Famo.us, and is the sort of thing you do a lot in jQuery.) E.g. vanilla Famo.us feels a lot to me like creating a web app with pure jQuery and no HTML template (okay, let's create a 'div', let's append it to another 'div', let's set a style on it, let's append all of that to our 'body', and voila! an app!—replace 'div' with 'view,' and 'body' with 'context' to really drive that home.) This recognition is a big part of what drove the creation of F/A's templating system in the first place.

So when it comes to manipulating vanilla Famo.us components that are created in a Famo.us/Angular template (fa-view and co.) we looked toward how Angular handles manipulating things in the DOM. Essentially, you can 'look up' an element in a directive's link function (either with $()/querySelector or by dealing with the element passed into a link function) and then manipulate those elements in the code. $('#my-cool-dom-node').css('backgroundColor', 'red'); Fortunately, we were able to create a way to grab a reference to a Famo.us component based on the DOM node that created it. I.e. there's a way to go from $('fa-modifier#my-cool-modifier') to having a reference to that modifier. The chain of code needed to dig it up gets a little long-winded, so we exposed $famous.find() to make this easier. E.g. for the template:

<fa-app>
  <fa-modifier id="my-modifier">
    <fa-surface id="my-surface">
      Hey there
    </fa-surface>
  </fa-modifier>
</fa-app>

This will get a reference to that modifier:

var myModifier = $famous.find('#my-modifier')[0].modifier
myModifier.transformFrom(//...

More here: http://famo.us/integrations/angular/docs/unstable/api/provider/$famousProvider/index.html

Note that just like in normal Angular, the DOM won't exist until a directive postLink, so $famous.find() won't resolve anything until then (i.e. this won't work in a controller, just like $() won't)

Now as for your solution: as I mentioned, it's a very interesting idea. In fact, I think the same idea could be applied to core Angular with normal DOM node manipulation—it is a convenient alternative to querying for getting DOM references. On the other hand, DOM querying is a rather robust solution that works with existing standards (classes, IDs, attributes, tag names) and that does not require additional markup in the template to work. Do you think there are aspects of faObject that aren't satisfied by $famous.find() and do you see compelling reasons that they should both exist?

jamwise commented 9 years ago

Thanks Zack for the detailed response.

I was aware of $famous.find() and ran into the obstacles you mentioned, which is why I thought of this solution. The find method actually works well if the markup is in the main html file, but I feel as though that will be a rare occurrence, at least it has been for my use cases. There are a few reasons I had felt best to avoid .find(), some may be out of ignorance, so I apologize is this all goes nowhere.

  1. The main reason I approached this is giving a bridge of sorts from the code in a controller or custom directive to the famous objects and to be able to do it when the DOM isn't yet ready. There could be a better solution by modifying the .find() method and I'm happy to look into that if this current proposed direction doesn't align with fa-angular's vision.
  2. Another reason is my interpretation of the Angular and Famo.us "way". Famo.us generally seems to strongly discourage DOM access, and although Angular encourages using markup, from my own understanding, it also strongly discourages direct access to the DOM from js. Although find() isn't suggesting DOM manipulation, it's also not in line with the pattern Angular proposes which is through directives, and Famo.us of course abstracts the DOM away completely. The fa-angular team has done a phenomenal job of merging these two approaches and you guys are no doubt much more experienced in all the above mentioned systems than I am, so I'm not sharing anything new, but I just want to share this as a background for my opinion on this matter: I think the use of classes and ids to refer to either famous or angular nodes should be avoided and the use of directives is a better and more familiar pattern. From my experience with Angular, modules built for it seem to show a strong aversion to using DOM selectors for anything and are reserved for assigning styles.
  3. This point I'm not fully sure of as I haven't read the code on Angular's jQuery implementation. But I think querying the DOM would be slower than storing some JS references.
  4. I think there is an inherent value of abstracting the process of finding and naming the famous objects. Especially because famous and fa-angular is growing and changing so rapidly right now, it might help for migration in the future.

These are my thoughts, but I'm not attached. Feel free to close this if it doesn't align with the vision and I'll instead try to understand .find() and see if I can resolve the main issue using that method.