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

Extended module functionality #845

Merged
merged 1 commit into from Jan 20, 2014
Merged

Conversation

jamesplease
Copy link
Member

Fixes the botched PR #844, which in turn adds the functionality described in #837.

-Modules now have the functionality of Marionette.extend.
-Upon creation, Modules will fire the initialize function, if it exists
-You may also pass a custom class to be instantiated as the Module when adding a new Module to the Application.

It breaks backwards compatibility in its current implementation, and has questionable unit tests.

var module = app;

// get the custom args passed in after the module definition and
// get rid of the module name and definition function
var customArgs = slice(arguments);
customArgs.splice(0, 3);
customArgs.splice(0, 4);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards compatibility is broken with this line here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't want to break compatibility you could try checking if ModuleClass extends the module prototype. This is ugly but it should work:

        if (ModuleClass && ModuleClass.prototype instanceof Marionette.Module) {
            customArgs.splice(0, 4);
        }
        else {
            customArgs.splice(0, 3);
        }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, but it doesn't seem to actually resolve the issue; it only reduces the likelihood of problems. This solution prevents the user from passing a Module as the first argument to their module. Attempts to do this would instantiate the module as that argument instead. I would agree if you were to suggest this to be an improbable situation, but it's a worrisome 'bug' to have floating around nonetheless.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea not sure what the best way to do this is, but the way it stands now won't work. Having to pass null if you want to add custom dependencies without a custom module class isn't good enough.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. That's just another conditional, right? Getting even more hideous, one might try:

    if (ModuleClass && (ModuleClass.prototype instanceof Marionette.Module) && !(ModuleClass instanceof Marionette.Module)) {
        customArgs.splice(0, 4);
    }
    else {
        customArgs.splice(0, 3);
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, taking @cobbweb's observation further, you need to apply this check more broadly, otherwise you'll end up attempting to new up the 4th parameter - which can be anything - in _getModule. Perhaps it's more appropriate to leave ModuleClass out of the signature and define it locally IFF arguments[3] is a Module definition.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccamarat, I think any attempts to determine whether to slice at 3 or 4 will be fallible in principle; no amount of conditionals can determine if the user is trying to pass an argument or the object to be instantiated. While I wish it was possible, I think @cobbweb's solution to use the object literal definition is the way to go here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

@jamesplease
Copy link
Member Author

@samccone, I removed the .gitignore. Also, your last message suggested changing the initialize function to ensure the context was right. Did you mean to bind to this rather than passing it as an argument? Something like _.bind(this.initialize.call(this.options), this)?

@cobbweb
Copy link
Member

cobbweb commented Jan 9, 2014

Could you please fix up the commit message? :)

@jamesplease
Copy link
Member Author

@cobbweb
Sure thing.

@jamesplease
Copy link
Member Author

One idea to retain backwards compatibility would be to change the second argument of Application.module to accept a new type of object, rather than adding a third argument. Something like:

if (moduleDefinition instance of Marionette.Module)
   // You know that it's a new class of Module
else
 // handle it as it's done currently

This prevents one from adding an initializer at the time of creation, but maybe it's worth it for saving backwards compatibility.

@cobbweb
Copy link
Member

cobbweb commented Jan 9, 2014

Do we not want a way to specify a default module class in an Application? I was thinking something along the lines of:

var app = new Marionette.Application({ moduleClass: CustomModule })

app.module('Foo', function(Foo) {
  console.log(Foo instanceof CustomModule); // true
});

@jamesplease
Copy link
Member Author

That's fine, but not something I had been thinking about. If you'd like me to add something like that I can go ahead and do it.

Just to clarify, for my purposes what I've included in this PR is necessary; the custom module class needs to be a per-add basis. The way I plan to use this is to make those components I mentioned before. I might, for instance, have a pie graph component and a menu component. The user can install these whenever/however you'd like by adding the appropriate custom module to the application.

app.module('PieGraph`, { region: PieGraphRegion }, PieGraphModule);
app.module('Menu', { region:MenuRegion}, MenuModule )

The passed-in region tells the application where to put it. The custom module tells the application what to add.

But to return to your comment would you like me to add that default module class functionality?

@cobbweb
Copy link
Member

cobbweb commented Jan 9, 2014

Module definitions can also be a plain object so you can toggle startWithParent. Instead of trying to introduce a new argument, maybe add another property in there?

app.module('Foo', {
  moduleClass: FooModule,
  define: function(Foo) {
    // Foo instanceof FooModule
  }
});

Doesn't read or type as well, but is a safer code change.

@jamesplease
Copy link
Member Author

// Continuing this chat

@cobbweb, I agree, and I took a look at the source and I think you might be onto something with the object literal suggestion you made up there. I'll probably have a chance to look at it over the weekend.

@jamesplease
Copy link
Member Author

This new commit implements your idea @cobbweb, which doesn't break backwards compatibility.

It seems like a sound implementation to me, but I'd like to add another parameter to the object literal definition, which is initialize. This allows you to extend a module with an initialize function without creating a separate object beforehand. This seems subtly different than the define function, in that define is 'dumbly' added and executed, whereas initialize will override the prototype's initialize (if it exists). While subtle in nature, it would bring the module extension in harmony with the way every other object is extended in the Backbone/Marionette frameworks.


// Overwrite the module class if the user specifies one
if ( moduleDefinition ) {
ModuleClass = moduleDefinition.customModule || ModuleClass;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if customModule is the right name, I was thinking moduleClass might be better. ping @samccone

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

@cobbweb
Copy link
Member

cobbweb commented Jan 11, 2014

Yep this is looking much better now :)

I think we need to do some refactoring around how we handle the module definition being a function or a plain object but we can do this after the fact.

@samccone Do we not want a way to specify a default module class in an Application?

@@ -77,13 +77,21 @@ _.extend(Marionette.Application.prototype, Backbone.Events, {

// Create a module, attached to the application
module: function(moduleNames, moduleDefinition){

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary extra line

@cobbweb
Copy link
Member

cobbweb commented Jan 11, 2014

Could you update the commit message to something along the lines of 'Allow Modules to be extended'?

@jamesplease
Copy link
Member Author

I've done my best to remove all of the extra lines you mentioned and updated the commit message. I haven't added the moduleClass to the application, since it seems like you're waiting for @samccone's thoughts on the matter.

@cobbweb
Copy link
Member

cobbweb commented Jan 11, 2014

Awesome, thanks @jmeas Yea just waiting for more input re: default module class.

@samccone
Copy link
Member

Ok this looks really great. I am still concerned about this breaking something that I am not thinking of.
I do not use modules heavily in the apps that I build so I may be missing something.

The tests for modules are a bit light, so maybe before we merge this we should backfill a few tests according to https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.application.module.md

For example, will this still work with this new code?
var fooModule = MyApp.module("Foo", { startWithParent: false });


Also, we need to document all of these in the readme, and provide examples. (this can be a commit ontop of all of this)

Basically this is close, I just want to make sure this is non breaking.

👍

@jamesplease
Copy link
Member Author

Being concerned about it breaking backwards compatibility is important, and I'm willing to write more tests to show that it won't. But I'll need more examples, I think, of situations where you're worried about it breaking the compatibility.

The example you gave seems to me to be covered by 'when configuring a parent module with the object-literal 'startWithParent' to false, adding a child module, and starting the app' in the module.startWithParent spec, but perhaps not?

I'm also fine writing up the documentation once we've settled on the final implementation.

@samccone, do you have any thoughts on whether the Application should have a moduleClass property? (see here and here)

Also, do either of you have any thoughts regarding the initialize parameter I mentioned before?

@samccone
Copy link
Member

I do not think we need a default moduleClass for now, let's try and get a skinny version of this feature out of the door and then we can iterate.

and +1 for the initialize param

@jamesplease
Copy link
Member Author

Some details about this latest PR

  1. Refines the tests for the module initialize function a bit, making sure it only runs once instead of at all
  2. Lets you pass the initialize function as a parameter in the object-literal definition
  3. Passes the entire object-literal definition as the first parameter of the initialize function. This allows you to include arbitrary parameters (options) to your custom module, just like the way the object passed as the first parameter of _.extend() works
  4. Runs the initialize function immediately, even if startWithParent is set to false. This is a different behavior from the initializer functions, like the one specified by define.
  5. Another difference between initialize and define is that the latter is not passed the entire object-literal definition as arguments, nor are any other initializers for that object.

The fifth option there is one that's only slightly questionable to me, but I'm unsure if changing it makes any more sense.

Thoughts?

@jamesplease
Copy link
Member Author

And I've somehow completely messed up the rebase, including previous commits in my rebase. Yikes.

Update: Actually, I'm not even sure if it's messed up. The other commits are still listed separately, and my attempts to undo things isn't being successful. If any has time to look into this it'd be much appreciated; I feel a bit at a loss here.

@samccone
Copy link
Member

git reflog then reset onto your last valid SHA then push it up, I can help from there :)

@jamesplease
Copy link
Member Author

Hm, part of the issue is that any SHA I select and reset --hard onto still causes the push attempts to respond Everything up to date.

@jamesplease
Copy link
Member Author

That may be the best I can do. Is that enough for you? It's still listing the 6 commits.

Update: Even if I rebase all the way back to when I cloned the thing, it still lists my commit + the other six commits under my un-pushed commits (via git log origin/master..HEAD).

@cobbweb
Copy link
Member

cobbweb commented Jan 13, 2014

I'm no Git Wizard but try this (assuming upstream is our main repo and master is your fork):

# clears all changes in your repo
$ git reset --hard upstream upstream/master 
# updates your repo to the latest stable
$ git pull upstream master 
# merge in your change
$ git cherry-pick 20b309ee363c197e6ef4557fbb4a3f915e9c59ee 
# force push
$ git push origin master -f 

In the future it's good practice to make a branch for your PR :)

@jamesplease
Copy link
Member Author

I made a first pass at the docs. Let me know what you think.

MyApp.module("Foo", {
startWithParent: false,
initialize: function( options ) {
// This code is immediately executed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe demonstrate setting a property on this, and then reference it in in the define function below? This shows how they're both called with the module as the context.

@jamesplease
Copy link
Member Author

I accounted for your suggestions, @cobbweb. If there's nothing else I can squash this into 2 commits (docs & code) or one to be merged.

@jamesplease
Copy link
Member Author

This PR has been squashed to have just two commits: one for the code, and the other for the README. From my perspective it looks ready to be merged. If you'd rather it be one commit just say the word.


```
MyApp.module("Foo", {
moduleClass: CustomModule
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add an example define option here too? So readers can see you can specify a custom module class and a definition.

@jamesplease
Copy link
Member Author

✓ Sinon spies utilized

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

Can you rollback and rebase off master instead so there's no merge commit?

});

it("should only run the latest initialize function once, and not the prototype initialize", function(){
expect(initializer.callCount).toEqual(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect(initializer).not.toHaveBeenCalled()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@jamesplease
Copy link
Member Author

Got rid of the merge, and accounted for this comment

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

Can you squash the two doc commits too please?

@jamesplease
Copy link
Member Author

You got it. Did you want me to do anything with the Jasmine->Sinon commit or message?

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

Yea sorry, can you squash that too please

@jamesplease
Copy link
Member Author

Do you want two commits total? Original + docs/sinon, or just one with everything?

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

Everything in one sounds good :)

@jamesplease
Copy link
Member Author

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

Thanks @jmeas!

Ping @samccone looking good now. The fact he hasn't changed any existing tests gives me good confidence there's no BC breakage. Module is tested quite thoroughly as well :)

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

Out of curiosity, what happens if you do something like this?

var CustomeModuleOne = Marionette.Module.extend({});
var CustomeModuleTwo = Marionette.Module.extend({});

App.module('TestModule', {
  moduleClass: CustomeModuleOne,
  define: function() {
    this instanceof CustomeModuleOne
  }
});

App.module('TestModule', {
  moduleClass: CustomeModuleTwo,
  define: function() {
    this instanceof what // ????
  }
});

@jamesplease
Copy link
Member Author

At first the situation you proposed might seem interesting, but I don't think it is, really. The previous behavior of adding modules was to ignore instantiating a new module when it already exists, and nothing has changed with this update. What this means in this situation is that the moduleClass parameter in the second request is ignored, and it just tacks on the definition to the already-instantiated CustomModuleOne.

Take a gander at this line of code to see what I mean. Everything in the if statement is ignored when the module already exists. The customModule parameter only really does anything within that statement. So, because we already have a module by this name, it is completely ignored in subsequent calls to module where the name is the same.

Update: It wouldn't be hard to add a unit test showing this, but, again, I don't think it's all that surprising a behavior.

@cobbweb
Copy link
Member

cobbweb commented Jan 17, 2014

@jmeas Yea I figured something like that would happen. As long as it doesn't fall over that's all good.

Custom modules can now be created using the extend method, and then attached to an Application. Documentation and tests were added for this new functionality.
@jamesplease
Copy link
Member Author

@samccone I adjusted the source according to your suggestions, with a few tweaks that I thought were necessary.

Namely, _.extend() must have an object as the first argument if any of the later arguments are objects. If the first value is undefined it will throw an error. In the situations where {} is the first argument, it is done so because the second argument can conceivably be undefined.

@jamesplease
Copy link
Member Author

Is there anything else I can do for this PR? 😄

@samccone
Copy link
Member

nope, going to get this version up on a few projects I have for testing, then 🚢

samccone added a commit that referenced this pull request Jan 20, 2014
Extended `module` functionality
@samccone samccone merged commit 3ad4a65 into marionettejs:master Jan 20, 2014
@cobbweb
Copy link
Member

cobbweb commented Jan 20, 2014

💃

@jamesplease
Copy link
Member Author

👏

Thanks, you two!

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

Successfully merging this pull request may close these issues.

None yet

4 participants