Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mixins? or provide a semantically meaningful API to execute behavior methods within a view? #1491

Closed
mxriverlynn opened this issue Jun 17, 2014 · 26 comments

Comments

@mxriverlynn
Copy link
Member

I have a need for what I typically think of as a mixin. In this case, it's a simple "loading..." message with a spinner gif. typical stuff, nothing special. but i want a re-usable component for this - one that i can display within any other view that needs it, when it needs it.

I thought, "behaviors!" - but it doesn't work the way I hoped.

If I define a method on my behavior, I have no way of invoking that behavior directly from within my view.

window.Behaviors.ShowLoader = Marionette.Behaviors.extend({
  showLoader: function(){
    // do stuff to show it, here
  }
});

Marionette.ItemView.extend({

  behaviors: {
    ShowLoader: {
      message: "loading ..."
    }
  },

  events: {
    "click .whatever": "clickedIt"
  },

  clickedIt: function(){
    this.showLoader(); //=> undefined is not a function
  }

});

I was hoping to be able to call the method directly. But as @samccone pointed out to me, this isn't possible. I understand that behaviors are sandboxed and that rocks. I like it. But now I want a semantically meaningful way of executing the showLoader function.

In a chat with Sam, he gave me some options - and I don't like any of them. They all break semantics, and/or tightly couple the developer's brain to the knowledge of how behaviors are implemented in order to use events / triggerMethod to make it work.

I'm picky about semantics. They are important. I want a semantically meaningful way to do this.

What I really want is a mixin - by name and by semantics. But I don't know of an easy way to make that happen in Marionette, right now, and behaviors were the closes thing. So I'm trying to abuse behaviors.

What's a better way to do this? Or is there a semantically meaningful API that can be added to behaviors, to allow a method to call behavior methods?

FWIW, here's the IRC log from my chat w/ Sam

derickbailey
is there a way to make a behavior's method available on the view that is using the behavior?
samccone
like to call a behavior directly?
derickbailey
my behavior has a "showLoader" method on it
samccone
yep
derickbailey
i want to call "this.showLoader()" from within the view that is using the behavior
samccone
word the "way" to do it
samccone
would be to change showLoader to...
samccone
onShowLoader
samccone
and triggerMethod("showLoader")
samccone
on your view instance
derickbailey
ugly :-/ should have some kind of "exports" from the behavior to make method easily available on the view
derickbailey
but will do for now
samccone
then your view is tightly coupled to the view
derickbailey
i'm ok with that
samccone
we did this on purpose to force separation between behaviors and views
derickbailey
it's a mixin
samccone
"proxy bus"
samccone
it does not mutate the view class
samccone
this way you can stack and mix behaviors without worries about the view instance
derickbailey
yeah
derickbailey
but it limits the usefulness of behaviors for situations like this
samccone
the evented interface acts as your interface layer
derickbailey
it's just an unintuitive API and an unintended use of triggerMethod
derickbailey
it would be better to have a semantically meaningful API to work with the behaviors
samccone
open to ideas on it. also would be interested in seeing your use case.
derickbailey
ajax loader / spinner image
derickbailey
i want a re-usable "showLoader()" and "hideLoader()" method
derickbailey
behaviors seem like the perfect place to do it
samccone
direct invocations of behaviors seems like it could be not the best use for behaviors
derickbailey
maybe i'm abusing behaviors?
samccone
when do you want to show the loader
derickbailey
it varies
samccone
on some sync or fetch event on your model?
derickbailey
sometimes on a button click
samccone
ok
derickbailey
some times on model fetch
derickbailey
sometimes on ... whatever
samccone
seems likeeee an agnostic event on the view would be a great way to normalize it
samccone
that your behavior can react to
derickbailey
again, unintuitive api
derickbailey
and wrong semantics
derickbailey
i don't like this.trigger("show:loader")... that's a command, not an event
derickbailey
i'm picky about semantics
samccone
sure  this.trigger("loading")
samccone
onLoading => ....
samccone
but you should file an issue
samccone
I am sure the team would love to present their use cases / approaches
derickbailey
yeah, there are plenty of "technically, it works" solutions... but not a clean one that i see
samccone
For me the the separation helps enforce behaviors
derickbailey
i agree... but now i want something more :)
@jasonLaster
Copy link
Member

Thanks for bringing this up @derickbailey.

Without knowing too much about the problem you're trying solve, I'd suggest a loadable behavior that responds to events like loading:start, loading:finish, loading:fail. The behavior has access to the view and can make intelligent decisions for what to do with that information. Also, it could itself emit updates through events like loading:show, loading:hide. Not sure about the utility there, but it could help further communication.

We've been able to use this event decoupling to build out nice UIs like forms where a behavior listens to form ui events like input keypress, select change, updates a form view model, and emits events like field:change, field:valid, field:invalid. Then it's up to the view or in the case of validation a validation behavior to hear the field:valid and field:invalid event and respond.


With that said, there are cases where a shared layout is a nice pattern or another type of delegation like composition makes sense. Can you provide more details on your example use case? I'd like to have a better idea of what you're trying to build.


One more thought, when I look at your behavior ShowLoader I see a semantics mis-match with the idea of behaviors built around interactions...

behaviors

Correct me if I'm putting words in your mouth, but I see you trying to encapsulate the idea of showing a loader, when with a behavior the semantics would be closer to handling events around loading. The distinction is subtle, but with show loading you're sharing a common implementation for showing and hiding loaders so it makes sense to mixin showLoader and hideLoader functions. In the case of a Loadable behavior you're teaching the view to react to events around loading...

@mxriverlynn
Copy link
Member Author

I see a semantics mis-match with the idea of behaviors built around interactions...

yes, this.

what i really want is a mixin... but i don't know a way to do this. so i'm trying to abuse behaviors, and it's breaking all the important semantics. "it works" - but not in a way that I'm happy with.

have any other suggestions for making this work, not using behaviors?

@jasonLaster
Copy link
Member

This might be a bust, but one suggestion would be to "import" a single function or module as a utility. Here's an example w/ require, if you're not using a module system this could feel ugly.

require([], function(Marionette, showLoader) {
  return Marionette.View.extend({
     ShowLoader: {
      message: "loading ..."
    }

    showLoader: showLoader
});
require([], function(Marionette, loaderUtil) {
  return Marionette.View.extend({
     ShowLoader: {
      message: "loading ..."
    }

    showLoader: loaderUtil.showLoader,
    hideLoader: loaderUtil.hideLoader
});

This is a simple "mixin" strategy for just importing functionality. It's nice because it's verbose. The drawback in my mind is that we don't have a separation of state. I could see supporting a pattern that encapsulates some of the benefits of composition, which would look like this if everything were written out, but could be dried up.

 Marionette.View.extend({
     ShowLoader: {
      message: "loading ..."
    },

    initialize: function() {
      this.loader = new Loader(this);
    }

    showLoader: function() {return this.loader.showLoader()},
    hideLoader: function() {return this.loader.hideLoader()},
});

@STAH
Copy link

STAH commented Jun 17, 2014

Why not use Backbone.Advice?

@jasonLaster
Copy link
Member

@STAH, I think functional mixins are a very appealing solution. I was not aware of Backbone.Advice. Very cool!

I've been meaning to give Flight's mixin library a go for it recently: lib/advice.js api. @samccone and I were talking to Ben Vinegar of Disqus at jsConf a couple weeks ago about how they use Flight's mixins for a similar use case.

I'd like to see a couple examples of Functional Mixins in the wild to inform my opinion. Maybe a good starting off point would be to add a couple of examples in the cookbook under an expirmental mixin folder. There are a couple patterns in there for overlays and hotkeys, which could be used as a jumping off point.

@STAH
Copy link

STAH commented Jun 17, 2014

I'll provide some examples from my production code :)

@mxriverlynn
Copy link
Member Author

I'm right smack in the middle of this need, so I'll try out a few of these soon and report back.

@jamiebuilds
Copy link
Member

If you're looking for something lightweight, I just put this together for my company's applications today:
http://jsfiddle.net/thejameskyle/q75tC/

@jasonLaster
Copy link
Member

Great!

@STAH
Copy link

STAH commented Jun 17, 2014

Just one example:

App.ValidableViewMixin = function() {
    this.after('initialize', function () {
        Backbone.Validation.bind(this);
    });

    this.after('onClose', function () {
        Backbone.Validation.unbind(this);
    });
};

App.SaveCancelViewMixin = function(mixOptions) {
    this.addToObj({
        events: {
            'click [data-action=cancel]': 'onCancel',
            'click [data-action=save]': 'onSave'
        }
    });

    this.setDefaults({
        onSave: function (e) {
            e.preventDefault();
            e.stopPropagation();

            this.model.validate();

            if (this.model.isValid()) {
                app.main.execute(mixOptions.action, this.model);
            }
        },

        onCancel: function (e) {
            e.preventDefault();
            e.stopPropagation();

            app.main.back();
        }
    });
};

....

App.MyView = Marionette.ItemView.extend({
    // ...
}).mixin([
    App.ValidableViewMixin
]).mixin([App.SaveCancelViewMixin], {
    action: 'myModel:save'
});

@jasonLaster
Copy link
Member

Hey, @STAH - thanks for sharing those two patterns. They help highlight some of the distinctions for me between behaviors event forwarding, mixin object extending, and functional mixin wrapping.

I just put together an opinioneted refactor of the patterns you shared, with what seems to me to be a cleaner version. Notice that I'm just doing three things:

  1. enforcing a flat shape, properties hanging off of a single object
  2. separating mixin configuration in setupMixin
  3. putting view configuration in a mixins property so that it's like events, modelEvents

These changes, might well be bad. It's my first take at wrapping my mind around how this might look similar to other things that I'm used to looking at :)

ValidableViewMixin = {

    setupMixin: {
        'after': 'initialize',
        'after': 'onClose'
    },

    initialize: function () {
        Backbone.Validation.bind(this);
    },

    onClose: function () {
        Backbone.Validation.unbind(this);
    }
});


SaveCancelViewMixin = {

     setupMixin: {
        'addToObj': 'events',
        'setDefaults': 'onSave',
        'setDefaults': 'onCancel'
    },

    events: {
        'click [data-action=cancel]': 'onCancel',
        'click [data-action=save]': 'onSave'
    },

    onSave: function (e) {
        e.preventDefault();
        e.stopPropagation();

        this.model.validate();

        if (this.model.isValid()) {
            app.main.execute(mixOptions.action, this.model);
        }
    },

    onCancel: function (e) {
        e.preventDefault();
        e.stopPropagation();

        app.main.back();
    }
});

MyView = Marionette.ItemView.extend({
    mixins: {
        ValidableViewMixin: {},
        SaveCancelViewMixin: {
            action: 'myModel:save'
        }
    }
});

@STAH
Copy link

STAH commented Jun 17, 2014

It's sooo.... perfect ;)

@STAH
Copy link

STAH commented Jun 17, 2014

But to make things simple, "mixins" block should be placed outside of MyView declaration (like in my example).

@jasonLaster
Copy link
Member

@STAH right. I kinda like it - especially now that I have a better idea of the advice api :)

re: moving the mixins block outside of the declaration.
Generally my preference is to keep the view configuration on the top of the class because it seems the most important information for the reader:

View.extend({
   // configuration
   template: {},
   events: {},
   behaviors: {},

   // callbacks
   initialize: function(){},
   onX: function() {},

   // private methods
   _foo: function() {},

Mixins could be an exceptions to the rule here. One reason is that this will probably have to happen at runtime to modify the instance the way we setup behaviors as opposed to modifying the prototype. Curious what you think?

Just added two examples to the cookbook.

Both examples port the Dropdown Behavior, the first is the standard Advice api, the second is the sugared version with a small hack to make it work. Feel free to git clone the cookbook, bower install; static and play with them / add some more. I'd like to get a couple more examples of more interesting Functional Mixins into the cookbook so that others can see how they might use this pattern.

@cmaher
Copy link
Member

cmaher commented Jun 17, 2014

@jasonLaster mixins should probably be an array, since the order in which they're applied matters.

so, possible interface:

MyView = Marionette.ItemView.extend({
    mixins: [
        ValidableViewMixin,
        {
            mixinClass: SaveCancelViewMixin,
            options: {
                action: 'myModel:save'
            }
        }
    ]
});

@jamiebuilds
Copy link
Member

At this point it sounds like we're no longer describing a "mixin". Typically, a mixin is simply a way of defining multiple-inheritance or including methods from another class. The easiest way to do this in Backbone is simply:

_.extend(FooClass.prototype, BarMixin);

Which is something frequently seen in Backbone applications, eg. Backbone.Events.

If we're talking about "mixins", then there should be no options passed to it, and it shouldn't be implementing anything from Advice.js (before, after, etc).

MyView = Marionette.ItemView.extend({
  mixins: [
    ValidableViewMixin,
    SaveCancelViewMixin
  ]
});
// or to avoid modifying extend:
MyView = Marionette.ItemView.extend({
  // ...
});

MyView.mixin(ValidateViewMixin);
MyView.mixin(SaveCancelViewMixin);

If you are okay using the second (I prefer it), then you can use it today (along with inherit, and include) with this snippet http://jsfiddle.net/thejameskyle/ZLYnN/.

If we're talking about something else that exists in Advice.js or a similar library, I think that lies outside the scope of Marionette and you should just include one of those libraries.

@stephanebachelier
Copy link
Contributor

@thejameskyle well written. I also prefer the second option.

@jamiebuilds
Copy link
Member

@stephanebachelier Thanks


I think the important thing to note here (since this is why it came up) is: Behaviors Are Not Mixins™


Marionette.ItemView.extend({
  behaviors: { foo: { behaviorClass: Foo } }
});

Behaviors are an isolated set of interactions added to a class that are interfaced via Events. The parent class cannot and shouldn't be able to directly interface with its Behaviors. Doing so would break the paradigm.

Why: To encapsulate logic in an evented system


_.extend(MyView.prototype, {
  // mixing in ...
});

Mixins are a set of methods that can be arbitrarily added to any Object. A mixin can directly interface with its host object, and a host object can directly interface with a mixin. In fact, most mixins will overwrite the objects methods. Mixins can also sometimes contain state.

Why: To expose methods/state on any object


var MyView = Backbone.View.extend({
  // subclassing ...
});

Subclasses are classes that are extended via prototypal inheritance. They setup a prototype chain and allow you to reference parent classes via __super__ (don't do that).

Why: To abstract functionality into a hierarchy

@jasonLaster
Copy link
Member

@cmaher - I can see how order could bite you, but I wonder if we could encourage patterns where functional mixins were built to be order agnostic. The big advantage I see here, is that it would be one less thing the end user has to think about when adding her mixins to the class.


@thejameskyle yup - "Behaviors are Not Mixins". To build on what you're saying, we're really talking about delegation strategies:

  1. Inheritance: prototype chain (Backbone.extend)
  2. Behaviors: event proxying (M.behaviors)
  3. Mixins: prototype property merging (_.extend)
  4. Functional Mixins or Decorators: prototype property wrapping (Backbone.advice)

It's not clear how much Marionette core will support these types of delegation patterns. At the very least, it'd be nice to document them in the cookbook and guides with some best practices and tips for when each is appropriate.

We might also be able to support some small Marionette plugins like what you started working on the other day at work and shared with the jsfiddle.


@derickbailey - how is your thinking evolving?

@moimikey
Copy link
Contributor

some of our prod code:

do (Backbone, _) ->
  Backbone.mixin = (base, mixins...) ->
    _.defaults base::, mixins...

then usage at the bottom of a view:

Backbone.mixin Show.Clipboard, App.Mixins.ScrollingCollection

containing:

@MM.module 'Mixins', (Mixins, App, Backbone, Marionette, $, _) ->
  Mixins.ScrollingCollection =

@jasonLaster
Copy link
Member

makes sense. I can see how the namespaces work well App.Mixins.ScrollingCollection. Good example

@jamiebuilds
Copy link
Member

Ping @marionettejs/marionette-core. This issue has not been resolved for a long time.

@jasonLaster
Copy link
Member

I don't see us adding a formalized Mixin or Advice delegation strategy in the short term.

I think we could close this and add better content around delegation such as guide, cookbook recipe, or blog post.

@jamiebuilds
Copy link
Member

I think we could close this and add better content around delegation such as guide, cookbook recipe, or blog post.

👍 I vote for a recipe using Backbone.Advice

@jasonLaster
Copy link
Member

@mrwokkel
Copy link

mrwokkel commented Nov 4, 2014

For anyone still in need. To answer the initial question:
in your view:

behaviors:
  theBehavior:
    class: this

in your behaviour behavior:

methods:
  mymethod: -> 

initialize: ->
  _.extend @options.class, @methods

I also was in need of something more like a concern (like used in rubyonrails). I think it is up to the developer how it is used (e.g. for behavior only)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants