marko-js-archive / marko-widgets

[LEGACY] Module to support binding of behavior to rendered UI components rendered on the server or client
http://v3.markojs.com/docs/marko-widgets/
MIT License
142 stars 40 forks source link

Define Widgets and Tags via Classes as an option #91

Open kristianmandrup opened 8 years ago

kristianmandrup commented 8 years ago

Now that Node.js 4.0 is out and most other modern frameworks leverage the concept of Classes in Javascript, I think it would be very appropriate if Marko Widgets (and Marko Tags in general) supported class definitions as a (modern) alternative. You could even leverage decorators, for those using Babel.

module.exports = require('marko-widgets').defineComponent({
    template: require('./template.marko'),

    getTemplateData: function(state, input) {
        return {
            name: input.name
        };
    },

    init: function() {
        var el = this.el; // The root DOM element that the widget is bound to
        console.log('Initializing widget: ' + el.id);
    }
});

Could be defined as a class, like this.

import {Widget} from 'marko-widgets';

// if no argument, assume this pattern as default
@template('./template.marko')
export default class extends Widget {
  constructor () {
        var el = this.el; // The root DOM element that the widget is bound to
        console.log('Initializing widget: ' + el.id);
  }
  getTemplateData(state, input) {
        return {
            name: input.name
        };
  } 
}

Using class properties

// @widget - not sure if a widget decorator would make sense instead of subclassing?
export default class {
  template = './template.marko'
  ...

or pure ES2015 getter

// @widget - not sure if a widget decorator would make sense instead of subclassing?
export default class {
  get template() { return './template.marko' }
  ...

Looks pretty cool and concise to me :) The nice thing is that it would be easy to do widget inheritance as well. Decorators can be used to provide mixin like behavior if needed (like in React components).

patrick-steele-idem commented 8 years ago

Hi @kristianmandrup, that is kind of supported, but the problem is that the defineComponent(def) and the defineRenderer(def) functions add some additional _static_ methods that are important to rendering:

Those static methods won't get added if you just export a class. Instead, you would need to do the following:

export default class Widget {
    constructor(widgetConfig) {
    }
}

var renderer = require('marko-widgets').defineRenderer({
    template: require('./template.marko'),
    getTemplateData: function(state, input) {
        // ...
    }    
});

Widget.renderer = Widget.prototype.renderer = renderer;
Widget.render = renderer.render;

While it would be nice to just use the class syntax, I don't think there is a good way to avoid the boilerplate if you want to attach the static rendering functions to the Widget class.

kristianmandrup commented 8 years ago

The elegant solutions: ES7 decorators ;)

@Widget({template: '../cool/template.marko'})
export default class MyCoolWidget extends CoolWidget {
    constructor(widgetConfig) {
      super(widgetConfig);
    }
}

https://github.com/wycats/javascript-decorators

It is also possible to decorate the class itself. In this case, the decorator takes the target constructor. Since decorators are expressions, decorators can take additional arguments and act like a factory.

http://www.2ality.com/2015/01/es6-destructuring.html http://www.2ality.com/2011/11/keyword-parameters.html

Destructuring can be used to effectively "simulate" optional function arguments

// Widget decorator: adds static renderer function to class
function Widget({template = './template.marko', templateData = undefined} = {}) {

  return function decorator(constructor)
   var templateData = templateData || constructor.prototype.getTemplateData;

    var renderer = require('marko-widgets').defineRenderer({
      template: require(template), // "configuration then convention " ;)

      getTemplateData: function(state, input) {
        templateData(state, input);
      }    
    });
    constructor.renderer = constructor.prototype.renderer = renderer;
    constructor.render = renderer.render;
  }
}

Pretty cool I should say :)

kristianmandrup commented 8 years ago

Hey, I'm trying a small experimental project using ES7 with marko templates.

https://github.com/kristianmandrup/marko-es7

However when I try your proposed pattern using

template: require('./template.marko')

I get an error:

SyntaxError: /Users/kristianmandrup/repos/test123/marko-es7/dist/template.marko: Unexpected token (1:6)
> 1 | Hello $data.name!
    |       ^
    at Parser.pp.raise (/Users/kristianmandrup/repos/test123/marko-es7/node_modules/babel-core/node_modules/babylon/lib/parser/location.js:24:13)

Looks like the babel parser is trying to parse it. Should just be "required" by the regular node require and transformed into valid javascript. I think you've added a special require hook for loading/transforming .marko files? How do I ensure that hook is being used without babel interference?

Thanks ;)

kristianmandrup commented 8 years ago

I kinda have it working now, except I'm a bit lost on how to test this infrastructure properly :P Any assistance would be greatly appreciated!! :) Please see my project.

Test https://github.com/kristianmandrup/marko-es7/blob/master/test/widget_test.js#L49

template config https://github.com/kristianmandrup/marko-es7/blob/master/lib/widget.js#L19

use of template https://github.com/kristianmandrup/marko-es7/blob/master/lib/index.js#L19

How do I access and use the template correctly here?

patrick-steele-idem commented 8 years ago

Unless I am missing something, the user should not be implementing the render(input, out) method for a UI component. Nor should the user be accessing the template directly. The user should just be implementing the getTemplateData(state, input) method.

Might be premature to use ES7, but interesting none the less. Personally, I am wary of decorators since I think they were abused in languages like Java (i.e. Java annotations).

patrick-steele-idem commented 8 years ago

Hey @kristianmandrup, were you able to come up with a good solution based on ES7 decorators?

kristianmandrup commented 8 years ago

Since I don't know the internals of defineWidget it was rather hard to debug given the limited time I was willing to throw at it. Mainly I just setup the babel infrastructure. I had problems requiring the .marko template correctly and not sure about my test infrastructure either. Would require a joint effort for ~30 mins I would think...