pugjs / pug

Pug – robust, elegant, feature rich template engine for Node.js
https://pugjs.org
21.68k stars 1.95k forks source link

Evolution of mixins - proposal / draft #1633

Open Artazor opened 10 years ago

Artazor commented 10 years ago

Hi @ForbesLindesay and jade-community! I would like to propose or discuss the impact (features vs. performance) of the following refactoring (and new features). Proposed refactoring is conservative in terms of public api - all existing unit tests are passed.

TL;DR

  1. Let's make templates and mixins first-class values in Jade in a clean sound and backward-compatible way:

    +[expression_that_evaluates_to_mixin](1,2,3)#id.some-class(href="#")
       span Hello from block
  2. And let's make templates and mixins more OOP friendly by allowing them to be late-bound on the same prototypal resolution principles as method calls:

    h1= this.title
    ul: each book in this.books
       li: .book
           +[book]cover()
           +[book.author]#{"bio" + "graphy"}()

    TL;DR-2 (longer)

Since subclassing is effectively constructed by prototype chaining, we can actually speak about classes, inheritance and overriding.

Imagine that you can somehow attach templates to your classes and use this reference to access current instance for accessing properties, calling methods and rendering other member mixins.

Example:

class Person
function Person(first_name, last_name, photo_url) {
     this.first_name = first_name;
     this.last_name = last_name;
     this.photo_url = photo_url;
}

var person_mixins = jade.mixinsFor(Person);

person_mixins["display-name"] = jade.compileMixin(src1);
person_mixins["info"] = jade.compileMixin(src2);

where src1 and src2 are:

//- mixin Person::display-name (src1)
span.display-name
    span.first-name= this.first_name
    span.last-name= this.last_name
//- mixin Person::info (src2)
.person
     img.photo(src=this.photo_url)
     +[this]display-name
class Agent
function Agent(code_name, photo_url) {
     this.code_name = code_name;
     this.photo_url = photo_url;
}

var agent_mixins = jade.mixinsFor(Agent);

agent_mixins["info"] = jade.compileMixin(src3);

where src3 is:

//- mixin Agent::info (src3)
.agent
     img.photo(src=this.photo_url)
     span.code-name= this.code_name
Mixin late binding demo

For given array of objects

var persons = [ 
      new Person("Thomas","Anderson","/img/neo.jpg"),
      new Agent("Smith","/img/smith.jpg")
  ];

the following code

ul: for p in persons
   li: +[p]info

will produce

<ul>
    <li>
        <div class="person">
             <img class="photo" src="/img/neo.jpg" />
             <span class="display-name">
                   <span class="first-name">Thomas</span>
                   <span class="last-name">Anderson</span>
             </span>
        </div>
    </li>
    <li>
        <div class="agent">
             <img class="photo" src="/img/smith.jpg" />
             <span class="code-name">Smith</span>
        </div>
    </li>
</ul>

Detailed proposal & implementation

1. Composable and decoupled from buf and runtime jade

In proposed implementation mixins are compiled into triple-curried functions. Their internal calling principle is changed from

 jade_mixins[name].call({attributes,block},args...)

to

(expression_that_evaluates_to_mixin)(args...)(attributes, block)(buf, jade)

where expression_that_evaluates_to_mixin could either be the same jade_mixins[name], or can be obtained from first-class mixin calling syntax (described below in p.4).

This will decouple mixins from template-wide globals:

and make them streaming-friendly since buf should provide only push method, that can perform arbitrary action on the chunk being emitted.

Also it eliminates usage of this for passing arguments and blocks (it was very clever and elegant trick but I believe it can be sacrificed for the sake of features discussed below).

2. this respectful

The implementation keeps this reference between currying steps and across all the template. Keeping this reference is important for proposed features. Also it involves minor change in with module that was recently merged into master: https://github.com/ForbesLindesay/with/commit/b752d06f84c28e30938819e53a8621dff3bb9cf3

Also it means that blocks should be internally compiled as

function(buf, jade) { ... }.bind(this)

and called as

block && block(buf, jade);

3. Templates as mixins

Implementation introduces new exported method compileMixin

var template = jade.compileMixin(src,  {args: "a,b,c", filename: "my-mixin.jade"});

that will compile source in the form of mixin proposed above. The form is context-independent since buffer and runtime are provided as parameters. Possible call of the compiled mixin could look like the following snippet:

template(1,2,3)({href:"#"})({
    push: function (chunk) {
       console.log(chunk); 
    }
}, Object.create(jade.runtime));

4. Mixins as first-class objects

We extend syntax of calling mixins allowing them to be first-class citizens of Jade

   +[expression_that_evaluates_to_mixin](args...).some-class(href="#attributes)
           span Hello from the block

In the following snippet

  +[obj.renderSomeView]

the mixin stored in obj.renderView is called in the context of obj like normal OOP method. But here we see undesirable fusion of two aspects in obj — it provides data context for rendering as well as rendering methods themselves. To separate these aspects we introduce notion of rendering_prototype described in the next section.

5. Rendering-prototype

We introduce notion of rendering_prototype that is a property of object's constructor like normal prototype, but contains jade's mixins as members, thus original prototype is not polluted. Also rendering prototype covariates with ordinary prototype. This achieved by two tiny helper functions

    // attach rendering prototype to constructor
    jade.mixinsFor(ctor) {
         // ensure covariance with ordinary prototype
         return ctor.jade_mixins = Object.create(
             Object.getPrototypeOf(ctor.prototype).constructor.jade_mixins || {}
         );
    }

    // retrieve mixins associated with given instance
    jade.mixinsOf = function(obj) {
        return Object.getPrototypeOf(obj).constructor.jade_mixins;
    }

To support render_prototypes syntactically we make further extension to mixin calling syntax

    +[expression_that_evaluates_to_object]mixinName...

or even

    +[expression_that_evaluates_to_object]#{expression_that_evaluates_to_name}...

that is compiled into

   jade.mixinsOf(
     jade_interp = expression_that_evaluates_to_object
   )[mixinName].call(jade_interp,...)(...)(buf, jade);

Let's consider example:

    // constructor
    function Greeting(name) {
         this.name = name || "Everybody";
    }

    // prototype methos
    Greeting.prototype.say() {
         return "Hello, " + this.name + "!";
    }

    // attach rendering prototype
    var greeting_mixins = jade.mixinsFor(Greeting);

    // render-prototype methods:
    greeting_mixins["say-hello"] = jade.compile("span= this.say()", {mixin: true});
    greeting_mixins["use-hello"] = jade.compile("h1: +[this]sayHello", {mixin: true});

    var test = jade.compile([
          "ul",
          "  li: +[this.a]say-hello",
          "  li: +[this.b]use-hello",
     ].join("\n"));

    result = test.call({
        a: new Greeting("Jade mixins"),
        b: new Greeting()
    });

that will produce

<ul>
    <li>
      <span>Hello, Jade mixins!</span>
    </li>
    <li>
       <h1><span>Hello, Everybody!</span></h1>
    </li>
</ul>

Note that template compiled with jade.compile still produces function that returns a string, but is this respectful.

Status of the implementation & discussion

Implementation is almost ready at my fork of jade https://github.com/Artazor/jade - this is only proof-of-concept. But i want to discuss proposed features before working on production-ready pull request.

I'd like to discuss the following items:

  1. Proposed syntax (I hope that proposed variants are more or less consistent - any opinions?)
  2. Notion of rendering-prototype
  3. Enabling usage of this in templates (the most controversial point)

For Nr.3 alternative implementation could use self quasi-variable that is already present in jade (instead of this). It will allow to:

The cost of this alternative is that

    +[object.memberMixin](1,2,3)

will be called in the context of the current self instead of object. But

    +[object]memberMixin(1,2,3)

will be called in context of object as it should. Here by the word "context" I mean the local value of self quasi-variable. May be this is reasonable difference since given cases come from different prototypes: the former comes form Object.prototype and the later — from Object.jade_mixins.

Would be glad to hear any opinions, Regards, Anatoly

Artazor commented 10 years ago

Rethinking all mentioned above, I decided to remove this related stuff in favour of self. It will make all the solution more lightweight and current-jade-flavoured. Working on pull request.

Still waiting comments and suggestions.

jamlfy commented 10 years ago

Mmmm think it's very similar to what I propose, in fact it can be complemented with #1566

Artazor commented 10 years ago

I have a pull request now #1654. It is simpler in implementation an does not involve any currying or other usage of this.

martindale commented 10 years ago

+1 for this. Specifically in the context of being able to do the following;

var express = require('express');
var app = express();

var jade = require('jade');
app.mixins['foo'] = jade.compileMixinFile('mixins/foo.jade');

app.get('/', function(req, res, next) {
  res.render('bar'); // mixin 'foo' is available in the `bar` template
});
nelsonpecora commented 9 years ago

:+1: this would be fantastic, and it solves a problem that currently (to my knowledge) doesn't have a good solution (reusing mixins across templates that aren't necessarily hierarchical)

martindale commented 9 years ago

@Artazor have you made progress on this? Even if it's a fork of Jade, this is immensely useful in a number of cases and I'd like to start using it.