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

Rename "controller" #499

Closed
gilbert opened this issue Mar 18, 2015 · 113 comments
Closed

Rename "controller" #499

gilbert opened this issue Mar 18, 2015 · 113 comments
Labels
Type: Breaking Change For any feature request or suggestion that could reasonably break existing code

Comments

@gilbert
Copy link
Contributor

gilbert commented Mar 18, 2015

My thinking is that "controller" is not sufficiently agnostic; the word comes with MVC baggage, yet it's use is very flexible
source

It's not that the view context can't serve as a controller, nor that it shouldn't if you choose to use MVC, but that it shouldn't be inferred to be limited to that concept by it's name.
source

Suggestions: scope, context, viewContext, viewctx, state, viewstate

Counter arguments:

While a controller can do many other things I think the MVC "baggage" is useful as it's a shared term with an understood meaning that gets the point across. Just because you can use it in other ways doesn't negate the fact that calling it a controller identifies the basic utility of it in a quick way that doesn't force you to learn a new term
source

Why the word "controller" is wrong?
"Controllers contain the interface between their associated models and views and the input devices
(keyboard, pointing device, time). Controllers also deal with scheduling interactions with other
view-controller pairs: they track mouse movement between application views, and implement
messages for mouse button activity and input from the input sensor. Although menus can be
thought of as view-controller pairs, they are more typically considered input devices, and
therefore are in the realm of controllers."
This is what the controller of mithril is doing right now. Mithril is one of the few frameworks that i really believe is using the MVC pattern correctly.
source

@l-cornelius-dol
Copy link

This has my strong, but tentative, vote.

My tentativeness comes from the fact that I have only employed variants of MVx in my code, and it's entirely possible that I just under-appreciate the value proposition of an MVC controller.

This proposal (as modified below by @pygy) has my strong, unequivocal vote.

My contention is that the thing called "controller" is broader than an MVC controller, and the name is too conceptually restrictive.

From my comment on the original thread:

My take is that at its simplest it's a rendering context explicitly injected into the view (and I love that explicitness, detesting JavaScript's non-syntactically-scoped this atrocity), which is why I think I prefer view-context or render-context. I prefer the former because it correlates to the view method, and it fits the actual use well, although it still inherits some small amount of MVC conceptual baggage (which is not necessarily bad, being limited to the concept of a "view" which I think is well understood and clear).

source

Supported in part by Leo's comment in the docs:

In client-side MVC, however, this dissonance doesn't exist, and controllers can be extremely simple. Mithril controllers can be stripped down to a bare minimum, so that they only perform a single essential role: to expose a scoped set of model-level functionality. [emphasis added]

source

My point being that if "Mithril controllers can be stripped down to [view-models]", then they are not specifically controllers in the MVC sense, nor specifically view-models in the MVVM sense, but both and neither.

And I want to stress my earlier comment that my experience writing code "has ingrained in me to use things the way they are designed to be used, or risk all hell breaking loose down the road". If Mithril's design intent is not to limit the use of the thing called controller, as the documentation currently states, then it should not be named "controller".

So, my answer to "What's wrong with controller", is that it's simply the wrong name for the concept it embodies in Mithril.

@tivac
Copy link
Contributor

tivac commented Mar 18, 2015

You linked my argument already, so just stopping by to reiterate my 👎 on changing this.

@l-cornelius-dol
Copy link

There's also the question of the name, for which My vote would be for ViewContext, if it remains a constructor, and viewContext if it is changed becomes a function that returns a view context.

Personally, I'd prefer the latter, because I vastly prefer closure-based objects, not this-based objects. However, I don't feel strongly about that since if it's redefined as a view-context a plain JS object is quite tolerable.

@l-cornelius-dol
Copy link

@tivac: Of course, renaming it to ViewContext does not stop you, in your code, providing a Controller as the view-context, using it as a controller, and employing MVC, including continuing to calling it ctrl in your view. So you lose nothing from the rename.

That would seem to make you ambivalent to the proposal rather than opposed to it; not to put words in your mouth. (Apart from then needing to do a global find/replace of "controller" to "ViewModel", perhaps?)

@pygy
Copy link
Member

pygy commented Mar 19, 2015

I think that we need to use different names for the initializer function (currently called controller) and the object it populates (for now this, then ctrl).

{
    init: function(foo) {
        foo.data = m.prop()
    },
    view: function(foo) {
        return m(".bar", etc...)
    }
}

...where foo can be named differently according to the context (ctrl, scope, viewmodel, etc...), but should, by convention, have the same name in both functions.

@gilbert
Copy link
Contributor Author

gilbert commented Mar 19, 2015

Nice! that's a neat option I haven't seen before. I think this will allow Mithril to get rid of the new keyword entirely. Also, I like the symmetry between the controller/init and view function signatures.

One affected aspect that I can of is @lhorie's blog post on using Meteor with Mithril (a lovely combination, by the way). The reactive helper presented in the article would change only slightly:

// I renamed `instance` to `foo` to correspond to your example
var reactive = function(controller) {
  return function(foo) {
    var computation = Deps.autorun(function() {
      m.startComputation()
      controller(foo)
      m.endComputation()
    })
    foo.onunload = function() {
      computation.stop()
    }
  }
}

Leaderboard.controller = reactive(function(foo) {
  foo.players = Leaderboard.players()
})

Astounding. To me this just feels right. If I'm not mistaken, it also makes controller/init functions more composable.

This also sidesteps the problem of what to name it our misunderstood little controller. If we call this new function something like init, setup, or onload, the user can name the foo parameter whatever they want (ctrl, vm, etc).

@l-cornelius-dol
Copy link

@pygy : That's strikes me as a really promising idea. I very much like the symmetry of invocation in initializing the view-context and generating the view. And it removes Mithril from having an opinion about what role the context fulfills.

I agree with @mindeavor, that it has a "feels right" appeal to it. And notice his example uses it as a controller with absolutely zero cognitive impedance.

I presume the Mithril call then becomes, using current module as an example:

m.module = function(root, module) {
    ...
    var isPrevented = false;
    ...
    if (!isPrevented) {
        m.redraw.strategy("all");
        m.startComputation();
        roots[index] = root;
        var currentModule = topModule = module = module || {};
        var context = {};
        if(module.init) { module.init(context); }
        ...

Where the name "context" is entirely up to Leo and is never seen externally; heck, it could remain controller, for all we care, though leaving it as that internally might be too dissonant to the actual use and documentation.

As well, logically then there's never a "dummy" context like we now have with the anonymous controller, only an empty one. This also feels cleaner, even though only really a semantic difference.

I would stick with init rather than onload, because I think onunload belongs as a property of the view-context and that would avoid the sense that onload and onunload should be in the same object but are not.

In terms of 0.x compatibility, it would be simple for Mithril to have a createContext function:

        var context = createContext();
...

function createContext() {
    var context;
    if(module.controller) {              
        context=new module.controller(); //FIX: 1.0 make throw; 1.1 Remove
    } else {                             
        context={};
        if(module.init) { module.init(context); }
    }
    return ctx;
}

I believe I give this idea a solid thumbs-up. It's the best proposal so far, I think.

@barneycarroll
Copy link
Member

I like this idea of having the unique instance as a passed in argument is great. Several distinct advantages:

  • view and controller (or whatever we want to call it) share the exact same signature: ( persistentObject, ...passedInArgs ). Considering the API for passing data through to components still takes a while to grok for some people, this can only be good.
  • Removes the superfluous cognitive burden of situation where a controller-less component's view is passed an instance of… something or other… myComponent = { view : ( ctrl /* WTF? */, stuff, I, actually, want ) => m( 'blah' ) }

This really helps you think about the lifecycle properly and massively demystifies the hitherto 'magical' aura of controllers. When we accept that there's a persistent object you can access from redraw to redraw, and that there's an optional function that only executes on the first draw, 'controllers' such as they are sound a lot like the thing being asked for in #492. Taking the thinking in reverse, and looking at how we currently use config – which has an init flag to indicate the first run and a persistent ctxt object for your own use – and accepting that nested modules' controllers already execute in the view step (and we can deal with that), it suddenly becomes apparent that we may not need a separate controller method after all:

myComponent = {
    view : function( ctxt, user, data ){
        if( !ctxt.init ){
            ctxt.stuff = coolThingsWith( data );
            ctxt.name  = user.firstName + user.secondName;

            if( ctxt.stuff.pending ){
                ctxt.then( m.redraw );
            }
            else if( ctxt.stuff.wrong ){
                m.route( '/away' );
            }
        }

        return m( 'h1', 'Power overwhelming' );
    }
};

@barneycarroll
Copy link
Member

BTW, in tangential support of the 'get rid of this' campaign, does anyone want to see how eye-gougingly horrible it is to do partial application with variadic constructors? No? Too bad:

function partialComponent( component, arg1, arg2, etc ){
    var args = [].slice.call( arguments, 1 );
    var noop = function(){};

    return {
        controller : component.controller ? function partialCtrl(){
            // Javascript is horrible. This is how you apply arguments to a variadic constructor:
            return new ( noop.bind.apply( noop, [ component.controller ].concat( args ) ) )();
        } : noop,
        view       : component.view ? function partialView( ctrl ){
            return component.view.apply( ctrl, args );
        } : noop
    };
};

@lhorie
Copy link
Member

lhorie commented Mar 19, 2015

@pygy is the lack of return data intentional in your idea?

@l-cornelius-dol
Copy link

@lhorie : I believe the lack of return data is intentional, because it's not a constructor of foo, but an initializer of foo, where foo is a simple {} object. See my theoretical change to 0.29's m.module function.

@lhorie
Copy link
Member

lhorie commented Mar 19, 2015

to clarify, I'm asking because DI'ing the context object means you no longer have control over its creation, so, for example, you can't use the singleton factory pattern.

@gilbert
Copy link
Contributor Author

gilbert commented Mar 19, 2015

@lhorie Mithril could treat it similar to the way JavaScript treats the keyword new. If the function returns something, that something becomes the context. Otherwise, foo stays the context.

@l-cornelius-dol
Copy link

Mithril could treat it similar to the way JavaScript treats the keyword new ...

Provided Mithril explicitly documents this behavior.

My theoretical context creation function would become:

function createContext() {
    var context;
    if(module.controller) {              
        context=new module.controller(); //FIX: 1.0 make throw; 1.1 Remove
    } else {                             
        context={};
        if(module.init) { context=module.init(context); }
    }
}

simplified, inlined and removed in version 1.x to:

var context=module.init ? module.init({}) : {}; 

though, I believe that would require init to return the passed in object, so maybe:

function createContext() {
    var ctx={};
    return (module.init && module.init(ctx)) || ctx;
}

@pygy
Copy link
Member

pygy commented Mar 19, 2015

I hadn't thought it through with respect to singleton factories, but @mindeavour adresses it. That being said, that would be re-implementing a core JS functionality for the sake of ditching this. Dunno if it' worth it...

Another point, as raised by @barneycarroll on gitter is the ability to use instanceof on ctrl objects:

But this still depends on the user not having implemented anything like 'instanceof'
And the worst thing about that 'instanceof' thing is that I've thought of using it.
To qualify what kind of incoming navigation a new element should get depending on whether the element previously in place was of the same type

I don't understand what he means by "what kind of incoming navigation", though.

@lhorie
Copy link
Member

lhorie commented Mar 19, 2015

If the function returns something, that something becomes the context. Otherwise, foo stays the context.

That sounds like a description of what it currently does (with the exception that you create the object instead of Mithril)

function MyController() {
  return {foo: "bar"}
}

That doesn't warrant a breaking change, imho. We could talk about the merits of dropping this separately from the merits of dependency injecting the object (which is really just a variation of using this and new).

I'd argue that from the controller caller side, it's easier to .apply arguments when you don't need to .unshift an extra parameter at the beginning of the argument list, and that from within the controller, the snippet above is much more intuitive than attaching things to a injected dependency (and on a semi-related note, that DI thing is eerily reminiscent of Angular's $scope.)

@barneycarroll
Copy link
Member

At the risk of putting too much into this thread, I might take this opportunity to point out that I've recently become quite a fan of the config function's undocumented this, which is the virtual DOM representation of the element within scope. Because this (haha) is undocumented, I don't think there's any harm in shifting that reference to a fourth argument (pending this suggestion of mine to expose an extra step of the Mithril lifecycle to authors)

@l-cornelius-dol
Copy link

Let's not get side-tracked from the actual request, which is to rename controller to clearly indicate it's actual purpose, or, more properly, the desired purpose for which it's actually often used, and that is a context object for the view. That to me, is well worth a breaking change in 0.x, trivially addressed with a simple global find/replace.

The change from constructor to initializer is, in my mind, hugely beneficial, in (a) the way it conceptually defines this as a container object, and (b) the symmetry it introduces in the two properties of the module (soon to be component, hopefully), the context initializer and the view generator, and (c) in the conceptual simplicity of having a simple view context that parallels config and doesn't imply more than it should.

But the main point of the request is to widen the narrow and somewhat contentious concept of controller to the wider concept of context.

@gilbert
Copy link
Contributor Author

gilbert commented Mar 19, 2015

Angular didn't invent DI, and just cause they go overboard with it doesn't mean it's bad :)

But yes, we are getting off topic. If we want to stay backwards-compatible and JavaScript-intuitive, an alternative name for controller could be init or constructor. The docs could say Mithril uses the init function to initialize the component when you use m.module (or m.component)

@lhorie
Copy link
Member

lhorie commented Mar 19, 2015

I know Angular didn't invent DI and I'm not even opposed to DI in principle, I'm just saying that usage pattern seemed very similar to $scope in the sense that it feels like magic

Btw, constructor is already a non-enumerable property of objects in Javascript, FYI.

console.log({}.constructor === Object) // true

function Foo() {}
var foo = new Foo()
console.log(foo.constructor === Foo) // true

@l-cornelius-dol
Copy link

@nijikokun : Actually, I think far more correct is that the name "controller" sometimes perfectly suits what it does. Other times it "doesn't exist", that is, it's anonymous (in a pure view module), other times it's a view-model (MVVM; see my quote, earlier, from the Mithril docs, where Leo advocates it contains only a generic map named vm), still other times it's a presenter (MVP).

In actuality, the concept that embodies all these disparate things, is a "context for the view" which may be utilized in a variety of ways.

Even more importantly, the thing named "controller" isn't even the object, it's (currently) the "whatever-it-is" constructor, and (as very much a secondary idea in this thread) could be changed to be the "whatever-it-is" initializer by conforming to the same DI model as the view generator.

@l-cornelius-dol
Copy link

@lhorie,

it feels like magic

I have to disagree; as an explicit parameter of an init function it seems less magical than a constructor, and parallels the view function, which I think reduces cognitive load. As well it parallels the context of config.

The conceptual notion of a container for holding things for this instance of a module/component is a simple concept, and avoiding this, as the view does and in which context you've previously defended not using this avoids the nastiness of this reassignment within the same syntactic scope.

The only question before us, on this _secondary_ consideration is whether the loss of being a constructor vs. being a simple object, that is the loss of a specific type is material to the way this object is intended to be used. On that, I think not. But in the rare occasion that it is necessary, maybe allowing the init to return an object that might differ from that passed in (and which capability is explicitly documented), might suffice. Certainly, as a design choice, this is much less weird to me than a "constructor" which is really an "initializer of this" returning an object other than this, or returning anything at all (which capability I think is more a side-effect of the way JavaScript pseudo-objects evolved than a true design intent).

@barneycarroll
Copy link
Member

@lawrence-dol I apologise if this is deviating even further, but do you think it's right to keep mechanics considerations —and potential changes to them — in this thread, or would it be more helpful if we moved that out to a separate issue?

@pygy
Copy link
Member

pygy commented Mar 19, 2015

On gitter, @lhorie said

I'm seeing people on both sides of the fence, so let's see where this discussion goes
personally I like controller. It might be a fuzzy term that means a lot of different things, but that's precisely what controllers are: a fuzzy thing that can be used in a bunch of different similar patterns (mvc, mvvm, mvp, mvblah)

The trouble is that the function currently named "controller" is a controller (vm, presenter...) constructor/initializer rather than a controller-level entity as defined in the original MVC pattern. I found the terminology confusing, at first.

I actually wrote a post about it on the Google group, then removed it when I grasped its usage. I had refrained from bikeshedding on the name, but since we're doing it now ;-)

@l-cornelius-dol
Copy link

The trouble is that the function currently named "controller" is a controller (vm, presenter...) constructor/initializer rather than a controller-level entity as defined in the original MVC pattern. I found the terminology confusing, at first.

Exactly! So did I.

Furthermore, an MVC controller is not an MVVM view-model, nor an MVP presenter, etc. Worse, there's very little agreement on what constitutes a controller, even if you are using MVC. Whatever the thing initialized by what Mithril calls a "controller" is, it is not (always) the "C" of MVC.

Even Leo advocates so in the documentation and in "Put your controllers on a diet".

So the problem is two-fold; not only is the thing referenced a constructor for the object, the purpose of said object does not always (even often) conform to an MVC controller.

Also, allow me to point out that quite apart from the "it's not a controller it's a constructor" argument... there are those, like me, who don't use it as a controller and don't want it called "controller", and those who do use it as a controller for whom a more accurate universal name would do no harm.

@lhorie
Copy link
Member

lhorie commented Mar 20, 2015

My reservation with the word context is that it's about as meaningless a name as the likes of FooManager and Foo.run() when you don't have all of its surrounding context (pun not intended) loaded in your brain.

The confusion between "presenter", "view-model" and "controller" still falls back to the idea that they are things with nuanced differences but with similar roles, and for better or for worse, the word "controller" has become a bit of a catch-all term for the controller-like entity in systems with models and views.

I don't have any objections about init or initialize

@gilbert
Copy link
Contributor Author

gilbert commented Mar 20, 2015

As much as I like init, controller actually might be best for
marketability. People recognize MVC; it makes them feel comfortable. It
also makes it easier to differentiate from other similar libraries like
React.

Correctness does not always bring popularity, unfortunately.

On Thursday, March 19, 2015, Leo Horie notifications@github.com wrote:

My reservation with the word context is that it's about as meaningless a
name as the likes of FooManager and Foo.run() when you don't have all of
its surrounding context (pun not intended) loaded in your brain.

The confusion between "presenter", "view-model" and "controller" still
falls back to the idea that they are things with nuanced differences but
with similar roles, and for better or for worse, the word "controller" has
become a bit of a catch-all term for the controller-like entity in systems
with models and views.

I don't have any objections about init or initialize


Reply to this email directly or view it on GitHub
#499 (comment).

@leeoniya
Copy link
Contributor

the traditional MVC as a pattern for frontend-only is very murky with regard to benefits. declaring something as MVC for marketability when in fact the C is so gutted, repurposed and discouraged compared to what people expect is quite confusing. the flexibility of javascript and the paradigm of everything living in the same space doesnt leave much for the controller. i know of several devs who basically use Backbone without using its controller aspects because they say there's not much that it adds to the picture in a browser. it's easy to say that a shared context object can be used in several ways, one of which is the C in MVC or the VM in MVVM.

edit: i actually think that it's precisely the familiarity with the prescriptionist naming/expectations of Controller that makes it weird to use in non-MVC patterns where the role it needs to take on is sufficiently different.

@lhorie lhorie closed this as completed Jul 24, 2015
@lhorie lhorie added the Type: Breaking Change For any feature request or suggestion that could reasonably break existing code label Jul 24, 2015
@nmai
Copy link

nmai commented Nov 29, 2015

I'm in favor of renaming controller to something else too. Mithril is the first MVC framework that I've really dedicated myself to learning. But I realize it's not your typical MVC framework. Mithril doesn't attempt to force a certain programming paradigm on developers, thus I feel it should not use traditional naming conventions. It's a different beast entirely. I feel like a great deal of confusion could be avoided if we renamed controller.

Alternatively, we could add a page to the docs discussing the purpose of controller and demonstrating a few of the different ways it can be used. I might actually prefer this. It shouldn't be necessary to rename controller if we make it clear from the very start what the differences are.

@avesus
Copy link
Contributor

avesus commented Nov 29, 2015

It is just the controller view pattern:
http://blog.andrewray.me/the-reactjs-controller-view-pattern/
On Sun, Nov 29, 2015 at 9:20 AM Nick notifications@github.com wrote:

I'm in favor of renaming controller to something else too. Mithril is the
first MVC framework that I've really dedicated myself to learning. But I
realize it's not your typical MVC framework. Mithril doesn't attempt to
force a certain programming paradigm on developers, thus I feel it should
not use traditional naming conventions. It's a different beast entirely. I
feel like a great deal of confusion could be avoided if we renamed
controller.

Alternatively, we could add a page to the docs discussing the purpose of
controller and demonstrating a few of the different ways it can be used.
I might actually prefer this. It shouldn't be necessary to rename
controller if we make it clear from the very start what the differences
are.


Reply to this email directly or view it on GitHub
#499 (comment).

@pygy
Copy link
Member

pygy commented Dec 21, 2015

@barneycarroll raised this again recently on gitter, and, if it came again on the table: I just thought of this to provide an easier transition route:

For a few versions, support both the old {controller, view} and the new {init, view} syntax. It would enable users to migrate their apps peicewise, rahter than warranting a big rewrite.

@l-cornelius-dol
Copy link

@pygy : I've also previously raised the point of when to eliminate the "controller" and suggested that it be done at version 1.0 when all the 0.x "wood-shedding" is complete (or as complete as it's ever going to be).

@dead-claudia
Copy link
Member

Could I get a quick description of this proposed "init"/"view" syntax? I
haven't been on Gitter much today.

On Sun, Dec 20, 2015, 22:05 Lawrence Dol notifications@github.com wrote:

@pygy https://github.com/pygy : I've also previously raised the point
of when to eliminate the "controller" and suggested that it be done at
version 1.0 when all the 0.x "wood-shedding" is complete (or as complete as
it's ever going to be.


Reply to this email directly or view it on GitHub
#499 (comment).

@l-cornelius-dol
Copy link

@isiahmeadows : #499 (comment)

But it's well worth reviewing the entire thread.

@barneycarroll
Copy link
Member

So I'd been working with Ember for a month when I happened upon this article by Dan Abramov on distinguishing between elements, components and component instances in React. These terms are necessary when discussing the structure and operating logic of any modern JS MVC library. It struck me that despite components being functionally much simpler and having a much much smaller API surface in Mithril compared to either React or Ember, the type of conversation Dan was trying to elucidate is far easier to have when dealing with either of those frameworks than with Mithril, because Mithril mandates the extra overhead of grasping what a controller is, because a practical conversation that refers to the Mithril API requires reference to controller functions and controller instances.

My contention is that the initial cognitive load and learning curve of Mithril can be much reduced by simply doing away with controller or vm. My proposal is to have components be functions (classes if desired) whose only requirement is to return a view method. Under this proposal, the front page component example becomes:

// ES6 
class Demo {
    constructor() {
        this.pages = Page.list();
    },

    rotate() {
        this.pages().push(this.pages().shift());
    },

    view() {
        return m("div", [
            this.pages().map(page =>
                m("a", {href: page.url}, page.title);
            }),
            m("button", {onclick: this.rotate}, "Rotate links")
        ]);
    }
};

// ES5 
var Demo = Object.create( {
    rotate: function() {
        this.pages().push(this.pages().shift());
    },

    view : function() {
        return m("div", [
            this.pages().map(function(page) {
                return m("a", {href: page.url}, page.title);
            }),
            m("button", {onclick: this.rotate}, "Rotate links")
        ]);
    }
}, {
    pages : Page.list()
} );

// ES3
function Demo {
    this.pages = Page.list();
}

Demo.prototype = {
    rotate : function() {
        this.pages().push(this.pages().shift());
    },

    view : function() {
        return m("div", [
            this.pages().map(function(page) {
                return m("a", {href: page.url}, page.title);
            }),
            m("button", {onclick: this.rotate}, "Rotate links")
        ]);
    }
};

// This-less Factory with closured private references
function Demo(){
    const pages = Page.list()

    function rotate() {
        pages().push(pages().shift());
    }

    return {
        view : () =>
            m("div", [
                pages().map(page =>
                    m("a", {href: page.url}, page.title);
                }),
                m("button", {onclick: rotate}, "Rotate links")
            ]);
        }
    };
};

I'm hoping @ciscoheat will back this proposal as truer to the original definition of MVC, inasmuch as we are no longer misusing the term 'controller'.

To be clear: this doesn't change the fundamental lifecycle at all, and doesn't prevent users creating whatever objects they deem fit: if you want to bind this to a variable named ctrl or simply create an object in the component closure, that's fine. If anything you have more flexibility in how you structure a component.

@l-cornelius-dol
Copy link

@barneycarroll 👍 This is more or less where my intuitions have been leading me as well. Since I build closure-based objects and eschew the psuedo-class prototypical objects entirely, I would be on-board with this.

In the end, the so-called controller is simply a view context, created by Mithril and initialized by the application. I have many things which are view-only "components", where I inject the needed state in when I call it. I would also be totally happy with a component being an load / view / unload tuple.

But I think @lhorie sees components more microscopically, where a component is, for example, one row in a table. I, for my part, typically consider a row in a table to be part of a table component where a view-only row-renderer is employed in a loop. And I think @lhorie is also fairly strongly wedded to the idea that components are created and destroyed on demand by Mithril -- I am not sure that he doesn't have a point.

@barneycarroll
Copy link
Member

@lawrence-dol to be honest, any conceivable practical example can be written in either style. If you're concerned that this proposal invites complexity by providing a closure for otherwise pure (ie stateless, view-only) components, note that class and Object.create do not mandate constructors: in fact these structures make for 'purer' view-only components since the signature no longer mandates a reference to the leading ctrl argument (make that an arrow function and you have effectively enforced purity by preventing any reference to this)...

Having said that, extending the component with statefulness is easy: as you say, we can write a closured component that enables truly private state without the need to attach stateful references to a host object; alternatively (or in addition), all component methods get a stateful reference to this if they so desire (so you can kiss goodbye to painstakingly imperatively defining, attaching and binding your methods in a function call).

Seeing as current Mithril is duty-bound to provide an empty controller constructor if you don't and pipe in the reference to your view even if you don't want it, this implementation is not any more laden with unnecessary stateful functions by default — arguably the opposite. But when the requirement for statefulness crops up, it's both less convoluted and far more flexible. People with an ardent hatred of this and explicit return statements can even get by with nothing but arrows and free floating variables per instance.

@ciscoheat
Copy link
Contributor

@barneycarroll that looks great, I'm completely with you about doing away with the controller function, since the object itself is the Controller. Same with the vm, the View object can play that role.

In the 0.2.1 release I saw some regression for the Mithril/Haxe-library, and the issue has always been about instantiating the controller. So moving to a user-defined constructor (in all the variations you present) instead of a convention-based controller function, would only do good things for me in that sense.

@michaek
Copy link

michaek commented Dec 30, 2015

I think this proposal would make it easier for newcomers to introspect what's going on in the instantiation of a component and how they can work with the component as a container for whatever pattern works best. Sounds great to me.

@StephanHoyer
Copy link
Member

what about:

function someView() {
  return [
    'random string', // "random string"

    m('div', foo),  // "<div>foo</div>"

    () => 'view function', // "view function"

    {
      init: () => {foo: 'with init function'},
      view: (scope) => m('.component', scope.foo)
    } // <div class='component'>with init function</div> 

    function() {
      var bar = 'with revealing view'
      return () => m('.component', bar)
    } // <div class='component'>with revealing view</div>
  ]
}

Last one seems tricky. Can't imagine how to preserve identity.

@barneycarroll
Copy link
Member

@StephanHoyer trying to work out the execution logic. Are all functions encountered in a view executed (and their outputs, if they are functions, executed, recursively) up until they deliver valid virtual DOM, and the last function thus encountered re-executed (with any higher order function closured the first time it executes) on subsequent draws?

@StephanHoyer
Copy link
Member

@barneycarroll Yes. Something like this. Haven't thought it to the very end. Maybe the 3rd way should'n be possible. Can't find a situation, where this would be practical. In this case, functions in the VDOM are always considered as components that have to be called and should return a view function that returns a VDOM-element.

@StephanHoyer
Copy link
Member

I think the 4th version is also pretty useless without parameters. Currently the components can get args with m.component which I always found stange.

Maybe better (I think @lhorie tried this once when introducing components with ver 0.2.0):

function initComponent(arg0, arg1) {
  return {
    init: () => {foo: 'with init function' + arg0},
    view: (scope) => m('.component', scope.foo + arg1)}
  }
}

initComponent('000', '111') // <div class='component'>with init function000111</div> 

This would also allow to use with ES6 classes.

Having that said the closure version might look like this:

function closureComponent(arg0, arg1) {
  var bar = 'with revealing view' + arg0
  return () => m('.component', scope.bar + arg1)
}

closureComponent('000', '111') // <div class='component'>with revealing view000111</div>

It's pretty much syntactic sugar for the first on it can easily be achieved with a wrapper function:

function wrapClosure(fn) {
  return function() {
    return { init: fn, view: (closureView) => closureView.apply(null, arguments) }
  }
}

To shim this behaviour we can also provide a wrapper function (I think @lhorie showed this somewhere)

function wrapComponent(fn) {
  return m.component({
    controller: function() {
      var component = fn.apply(null, arguments)
      var scope = component.init()
      scope.__view = component.view
      return scope
    },
    view: function(scope) {
      return scope.__view(scope);
    }
  })
}

@barneycarroll
Copy link
Member

@StephanHoyer the interop wrapper function highlights the fact that we don't really need to petition for core Mithril changes to validate our ideas about better component structure.

Mithril is currently overburdened with non-trivial upgrade work, and last this conversation gained traction there wasn't sufficient energy to provide proofs of concept for these breaking change proposals. Even at v0.1, there was a massive sense of foreboding in deprecating any of the API.

I'm going to try my luck at implementing a framework-agnostic component library - spiritual successor to Modulator - using my proposal above. My plan is first of all to provide a totally generic component structure and consumer function that simply provides a scope for a view and can be plugged in to Mithril/Cito/DOMVM/React/whatever, and then to see about whether it's worth pursuing extensions to hook into lifecycles. I've tried the latter a few times, and it inevitably ends up with a lot of overhead replicating Mithril's internal logic, but it's easier than patching a branch of Mithril and valuable enough as an experiment (maybe it's worth the extra overhead just to have a nicer API, maybe the PoC can provide insights for Mithril).

I'd previously been toying with the idea of writing a component-conscious layer on top of Cito called Citoplasm, but the more generic Plasm will suit all contexts.

@nmai
Copy link

nmai commented Jan 3, 2016

@barneycarroll Even though I'm in favor of this proposal, I have to agree that this is a rather trivial issue and probably not worth the effort right now.

I was about to un-watch this thread, but the component library you mentioned caught my interest. Have you started on it yet? I'd love to see where this goes. Do you think you could make it work with Angular 2 as well?

edit: Just discovered your Plasm repo, looks good so far- will be checking up on it every once in a while!

@barneycarroll
Copy link
Member

@nmai I'm tentatively calling the lib Plasm. Repo is here — it's just a manifesto and an untested proof of concept ES6 file (with a dubious ES7 dependency) ATM. AFAICT Angular 2, like Ember, still insists on writing views in its own weird string-based template language, locking the author out of direct access to the interpolation logic, so you wouldn't be able to plug this in directly. As with Ember, what's actually going on at any point in time is so far removed from application code that you'd probably need a custom helper / directive to integrate this. We'll see if that becomes worthwhile. In any case, I digress. Please file issues there if you're interested.

@StephanHoyer I like the simple function returning a function system — removes the need to debate whether view is a better term method key than render etc ;). Having said that, the logic for variable functor depth could be tricky — you'd need to execute the outer function to determine whether or not it was the view or it was the closure containing a view function. That's not an immediate blocker (you will always want to execute a function the first time you encounter it, and subsequently you will have registered it along with its output so you'll know whether to re-execute it or its output to retrieve a pure or stateful view)… But it does add some overhead. Perhaps better to say a pure aka view-only component at its simplest is an idempotent functor returning the view function?

@tinchoz49
Copy link

@barneycarroll great manifesto!! i like it! that's the pure idea of components.

@pdfernhout
Copy link
Contributor

@barneycarroll @lawrence-dol While I like much about Mithril, the way it handles components seems awkward to me and creates surprises (like when top level components work differently than nested components or trouble thinking through what happens when components get made and updated). Mithtil controllers were also awkward to integrate with TypeScript classes (although doable via static methods as I explained here).

Components are one area where Mithril tries to do too much and so creates unneeded complexity in an attempt to support one specific use case for managing the component lifecycle. So, I can see the benefit of the Plasm approach or other ideas sketched above.

You may be interested in this example I made to go with the Maquette vdom:
https://github.com/pdfernhout/dojo2-maquette-demos/blob/master/hello-increasing-abstraction/hello-114-SpecifiedWidget/helloSpecifiedWidget.ts

Maquettte does not have built-in component support, but IMHO is better in that specific way for not trying to do it. See also the Maquette documentation on arrays from some ideas about a common issue which Mithril solves well in its own way:
http://maquettejs.org/docs/arrays.html

I'm not saying Maquette is better than Mithril in all ways. In pursuit of rendering speed, makes different tradeoffs many people would not like which make its API harder to use in some ways. But by doing less with components it actually does more in a way, since you just manage components yourself and call view/render methods yourself or with a helper function. With a small tweak to call a callable passed in or a view/render method on an object passed in to an "h" function (equivalent to "m"), I feel Maquette would be even more component friendly (similar to what the second helper function linked below does). Anyway, Maquette's simpler approach is something worth studying as an alternative approach for a future Mithril version in a future rewrite.

With that said, you can just ignore the Mithril component approach in nested components and DIY. Just make an instance of a class which has a view method and call the view method yourself in the middle of various "m" calls in some other view method.

Here are two versions of a "r" (for "render") helper function for DIY components done for Maquette but which could be used in a similar way for Mithril:
https://github.com/pdfernhout/dojo2-maquette-demos/blob/310969f75b0dad43d26b5d97b633815c72618269/hello-increasing-abstraction/hello-107-Components/helloComponents.ts#L64
https://github.com/pdfernhout/dojo2-maquette-demos/blob/de8154f414d4a93bb0f49266d59738ebbf3d1e56/hello-increasing-abstraction/hello-108-ComponentsMarkup/helloComponentsMarkup.ts#L77

@l-cornelius-dol
Copy link

I am trialing Leon Sorokin's DOMVM in my current project, precisely because it's completely paradigm agnostic (i.e. it's not MVx but can be if that's your thing) and (so far) seems completely amenable to a component-first architecture (something which is my thing). Leon also brings a solid commitment to minimalism, a clean and consistent API and to never introducing "tooling" for producing DOMVM, something I very much support since I do want to contribute. Lastly, DOMVM is well structured and module, but requires only simple concatenation of the bits you want -- toolkit free and modular, something I think is where Mithril's community failed (I mean that in the nicest way possible).

I too feel that Mithril doesn't get components right, though I still have several key projects that use Mithril. What's worried me a bit more is that Mithril has suddenly gone "dark" before reaching anything that could be considered a 1.0 release. I do like Mithril, component implementation aside, and I hope to see a re-imagined Mithril from Leo... but so far there's not much indication of that and my development must go on. I am also fairly heavily invested in Mithril, but not so much it can't be undone if warranted.

However, with all that said, I am inexpressibly grateful to Leo for introducing me to the concept of vDOM and pure JavaScript front-end development, before which I was seriously considering quitting my job over the prospect of having to play in the JS+HTML+CSS pool for the majority of my work. The Mithril approach of pure JS plus targeted CSS was a revolution for me.

@barneycarroll : Hmmm. Plasm looks nearly identical to DOMVM.

@leeoniya
Copy link
Contributor

it's not MVx but can be if that's your thing

Actually, I got curious myself if it was possible to mock Mithril's MVC-isms, args, contexts and global auto-redraw semantics in domvm. I focused on the feature set needed to be able to copy-paste the link rotator demo from Mithril's home page and have it "just work". Turns out that it was pretty trivial [1] [2]. The mithril adapter itself is tiny [3].

[1] http://leeoniya.github.io/domvm/demos/mithril-adapter/
[2] https://github.com/leeoniya/domvm/tree/master/demos/mithril-adapter
[3] https://github.com/leeoniya/domvm/blob/master/demos/mithril-adapter/mithril-adapter.js

@lhorie
Copy link
Member

lhorie commented Mar 29, 2016

@lawrence-dol how are you liking DomVM so far?

@l-cornelius-dol
Copy link

@lhorie,

how are you liking DomVM so far?

I am not nearly as far along yet as with Mithril in other projects, so it's a bit soon to tell, but so far it's making a lot of sense and most of the concepts translate well.

Components seem more intuitive, to me anyway, since each view-model is a component encompassing a DOM subtree and losing the MVC inferences makes me feel like I am not swimming against upstream anymore. It probably helps that I am in the camp that thinks MVC is irrelevant in a JavaScript app. Conceptually I like the idea that the view-model pieces tie the data-model and the application logic together in any way that makes sense, which is more or less what I am doing with Mithril anyway. I have a few component pieces that use Mithril which turned out really well and will be easy to convert to DOMVM should I elect in the end to stick with it.

The use of m() in templates is replaces with an array representing the component or element; at first I was certain it would be bracket-soup, but I resisted the temptation to create a function that just returned it's arguments as an array, and with a bit of attention to alignment and layout the templates remain very readable.

I think it also helps that it seems Leon and I are far more on the same page for the most part with our approaches to coding, tooling, modularization, and other things somewhat tangential to actually delivering a black-box library. He seems to care about names and the concepts the communicate as much as I do.

So the jury's still out, but it's looking likely that DOMVM is where I'll end up. The answer will only come with more time, doing actual coms and wiring together parts of the app as UI interactions happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Breaking Change For any feature request or suggestion that could reasonably break existing code
Projects
None yet
Development

No branches or pull requests