malgorithms / toffee

a NodeJS and browser templating language based on coffeescript, with the slickest syntax ever
MIT License
174 stars 10 forks source link

API partials #30

Closed hhsnopek closed 10 years ago

hhsnopek commented 10 years ago

Could you expand on this:

[err, res] = v.run {
   name:    "Chris"
   partial: (filename, vars) -> "Do something with #{filename} and #{vars} that returns a string"
}

is filename the path to the file; if so is vars then just for that partial?

I'm attempting to pass partials in like so, except I'm using the compile strRender and render methods

hhsnopek commented 10 years ago

As well so you have a better background on what I'm doing; I'm writing toffee an adaptor for accord; which is similar to consolidate.js and transformers.

malgorithms commented 10 years ago

Oops mid-clicked and closed . What is needed for it to work with accord? Just a certain exposed function? If so I can add it to what's exported

malgorithms commented 10 years ago

(Will answer your other q when I can get to a computer - on phone now )

hhsnopek commented 10 years ago

well most templating languages have different render, compile, helper functions so what we've done is made them similar. Basically I need to render strings, files (with and without partials), and compile strings, files (with and without partials). Currently I've gotten rendering strings and files down. I'm having issues add partials(that aren't included in the parent toffee file) and similar issues with compiling.

malgorithms commented 10 years ago

Could you expand on this

[err, res] = v.run {
    name:    "Chris"
    partial: (filename, vars) -> "Do something with #{filename} and #{vars} that returns a string"
}

is filename the path to the file; if so is vars then just for that partial?

Here's how partials work: in the most generic sense, partial in a toffee template is not a keyword or anything special:

<p>
   #{partial('foo.toffee', {name: "Chris"})}
</p>

is just like any other javascript function:

<p>
  #{JSON.stringify {foo: 100}}, or
  #{Math.random()}
</p>

So where does it get defined? If you use a Toffee engine, it will define a partial function for you and inject it as a variable (like any other) into your render calls. The Toffee engine will monitor your filesystem and recompile views whenever files change -- and when its partial is called, it will know what view to use. You don't need to use a Toffee engine, but that's basically its purpose: keeping views compiled on an a changing filesystem, without restarting your server or hitting the file system on every render call.

But if you pass your own partial function, the engine won't mess with it.

This in theory allows you to define your own partial function, which you shouldn't really need to, I hope. But if you do, it should work. I'm not sure anyone has ever done this, so if it has any probs, let me know.

As for your question:

partial: (filename, vars) -> "Do something with #{filename} and #{vars} that returns a string"

is filename the path to the file; if so is vars then just for that partial?

filename is the path to the file, and vars is the variable object you are passing to the partial. Remember, you are (re)defining this function:

<h1>#{partial 'title.toffee', {foo: 100}}</h1>

Does all this make sense?

hhsnopek commented 10 years ago

That clears up soo many of my questions! Thank you, I will keep this open if I have any issues

hhsnopek commented 10 years ago

I'm attempting to render and compile a string(with partials) like this:

toffee.strRender("<p>Hey there #{name}!</p><p>#{partial 'love-me.toffee', {label: 'toffee'}}</p>", {name: "earth"}, (err, res) -> return res)

Yet the output is this:

<div style="line-height:13px;border:1px solid #999;margin:10px;padding:10px;background-color:#fff;position:fixed;top:0;left:0;max-width:90%;z-index:9999;max-height:90%;overflow:scroll;">

    <pre>./<span style="background-color:#fde"><b>null</b>: Object #&lt;Object> has no method &#039;__partial&#039;</span></pre>

    <hr />

    <div style="font-family:courier new;font-size:12px;color:#900;width:100%;">
        <div style="border:1px solid #000;background-color:#eee;width:100%;">
            <div style="color:#333;width:100%">0 TypeError: Object #
                <Object>has no method '__partial'</div>
            <div style="color:#999;width:100%">1 at Object._l.partial (evalmachine.
                <anonymous>:81:23)</div>
            <div style="color:#000;width:100%">2 [on line 1] ./null</div>
            <div style="color:#999;width:100%">3 at _TMPL_ (evalmachine.
                <anonymous>:145:17)</div>
            <div style="color:#999;width:100%">4 at view.run (/Users/henry/develop/accord/node_modules/toffee/lib/view.js:197:17)</div>
            <div style="color:#999;width:100%">5 at Object.exports.str_render.exports.strRender (/Users/henry/develop/accord/node_modules/toffee/index.js:66:15)</div>
            <div style="color:#999;width:100%">6 at /Users/henry/develop/accord/lib/adapters/toffee.coffee:29:27</div>
            <div style="color:#999;width:100%">7 at compile (/Users/henry/develop/accord/lib/adapters/toffee.coffee:91:15)</div>
            <div style="color:#999;width:100%">8 at Toffee._render (/Users/henry/develop/accord/lib/adapters/toffee.coffee:27:14)</div>
            <div style="color:#999;width:100%">9 at Toffee.Adapter.render (/Users/henry/develop/accord/lib/adapter_base.coffee:24:19)</div>
            <div style="color:#999;width:100%">10 at Context.
                <anonymous>(/Users/henry/develop/accord/test/test.coffee:1448:26)</div>
        </div>
        <span style="background-color:#fde">
            1: &nbsp;&nbsp;&nbsp;&nbsp; &lt;p>Hey there #{name}!&lt;/p>&lt;p>#{partial &#039;love-me.toffee&#039;, {label: &#039;toffee&#039;}}&lt;/p></span>
        <br />
    </div>

</div>

I'm unsure why this is, I thought strRender would output with a partial included inside the file itself?

hhsnopek commented 10 years ago

after reading through some of your code there really isn't "compiling" of a template. It's just rendering views?

malgorithms commented 10 years ago

after reading through some of your code there really isn't "compiling" of a template. It's just rendering views?

There is a compile step, but by default it is on-demand. It takes your Toffee, converts it to coffee, then converts that to a javascript function which is fast and handles the render calls. This compilation is the most expensive part of Toffee, and it only happens once, leaving your view with a fast, "compiled" function.

But if you want to force this compile yourself, you can.

Example one:

v = new view 'This is a toffee template #{foo}'
[err1, res1] = v.run({foo:1}) # this'll be a little slow since it compiles on demand, internally
[err2, res2] = v.run({foo:2}) # this'll be fast as hell

Example 2

v = new view, 'This is a toffee template #{foo}', {
  cb: ->
     console.log "The view is compiled!"
     [err3, res3] = v.run({foo:3}) # this'll be fast as hell
}

Toffee exposes a `compileStr` function (I can't recall who asked for that), which takes a template string and returns a runnable function. Yet technically it still does the compile on demand. This seems like the best thing to me, but it doesn't have to work that way, and you can pre-compile as shown above.

Note you should have the latest Toffee (0.1.11) to do the pre-compiling, since I had a bug which I just fixed.
malgorithms commented 10 years ago

I'm attempting to render and compile a string(with partials) like this:

toffee.strRender("<p>Hey there #{name}!</p><p>#{partial 'love-me.toffee', {label: 'toffee'}}</p>", {name: "earth"}, (err, res) -> return res)

Yet the output is this: [etc.]

So there are two separate issues here:

(1) since you're not using a Toffee Engine, and you could be running this in any context (browser, etc.), you need to define your own partial function. For example, this works:

toffee = require 'toffee'

template = '''
  <p>Hey there #{name}!</p><p>#{partial 'love-me.toffee', {label: 'toffee'}}</p>
'''

vars = 
  name: "earth"
  partial: (filename, vars) -> 
     # right here you need to define a function that knows what the heck to do with a "filename"
     # and vars; keep in mind you could be targeting the browser or whatever
     return "hello world. Signed with love, #{filename}"

console.log toffee.strRender template, vars, (err, res) ->
  console.log err, res

Again, the Toffee Engine knows how to do this, because it is designed to be run in some context. If you just initialize a "view" with a string of toffee, it doesn't know anything about the file system.

(2) the reason you're seeing that block of HTML is that the default output for Toffee is to fake a good result but pretty print the resulting runtime stack. This is easily changed if you want it to reply with a real error:

vars = 
  name: "earth"
  prettyPrintErrors: false
hhsnopek commented 10 years ago

Ok, so I went through and I'm attempting to simplify what I have. I want to clarify what I'm doing first(thank you for being os patient with me!)

Engine: you could modify output options View: you must re-write the partial function for it to work.

EDIT: Does the engine compile?

malgorithms commented 10 years ago

EDIT: Does the engine compile?

The engine defines a partial function on any file it renders. Recall, partial takes 2 params: (1) the filename, and (2) the vars. An engine's partial works like this:

  1. translate the filename to an absolute path
  2. do I have a view cached for that path already? (engine keeps a dictionary of them)
  3. if not, create a view and put it in that cache
  4. call view.run on the vars

since view compiles code on demand, internally there is compilation going on.

To clarify, if you're asking if engine exports a "compile" function that just takes a string and generates a function, no...the problem here is that the goal of engine is to handle all the file system stuff, and a string isn't much context. An engine won't work in the browser, for example. But a view will.

In case you're wondering, Toffee can work entirely in the front end. If you have a directory of toffee files, you can just run the binary toffee some_dir -o foo.js and then do a in your browser. And yet partials work entirely without an engine. They do because each template, when compiled, goes into a dictionary, and partial is told to look in that dictionary.

hhsnopek commented 10 years ago

Got it.

Last issue I have is compiling a file with this method:

toffee.compile(hello.toffee)({name: 'world'})

I'm getting output of undefined

malgorithms commented 10 years ago

toffee.compile is designed to match early versions of express (when I first made it). It takes a string as an input and returns a function which takes a vars object and returns a string from that.

x = toffee.compile '''
 This is a template #{name}
'''

output = x({name: 'world'})

It doesn't use an engine, so it can't handle files. You could read the contents of a file yourself and pass it to compile. But I'd just recommend building a view with it instead, in that case.

hhsnopek commented 10 years ago

Would you be able to add a module to do this? it would be much cleaner if I could just call toffee.compileFile(file)(options)

malgorithms commented 10 years ago

How would it be different from the following? It seems like you can roll this pretty easily:

compileFile = (file) ->
  toffee.compile fs.readFileSync file

# then you can use it the way you want
compileFile(file)(options)

But this wouldn't define partial, of course, since it's not using an engine. Are partials supposed to work in this case or will you be passing a partials definition yourself?

hhsnopek commented 10 years ago

partials should still be pre-defined(I'm not touching them)

malgorithms commented 10 years ago

does this need to run in the browser or just in Node.js?

hhsnopek commented 10 years ago

just node :D

malgorithms commented 10 years ago

ah, ok. I was imagining a scenario where you were calling this compileFile function and had some alternative data source for the file contents. (This is how I implement Toffee+partials in the browser).

Does your compileFile function need to be sync, or can it be async? And what about the function it returns? Does it return a function that returns a rendered page or one that calls back with one?

i.e., would it work like this:

# async version
compileFile filename, (template) ->
   console.log "the template is ready"
   template {name:'foo'}, (err, res) ->
      console.log err, res 

Or like this?

# sync version
template = compileFile filename
console.log "the template is ready"
[err, res] = template {name:'foo'}
console.log err, res

The former, async style, is greatly preferred for performance reasons, since we are talking about File IO here, and your node process will be blocked in the latter version.

hhsnopek commented 10 years ago

async always, and it should return a compiled page

malgorithms commented 10 years ago

btw, this is why I tend not to export too many functions. Given the building blocks of Toffee's view and engine classes, you can do lots of different things. There's also the version where compiling is async, but running is sync, and vice versa. (Basically a 2x2 matrix of options here.)

In the example you gave, which was all sync, I think you could do something like this (this is a working program, try it! just make sure to make a foo.toffee too.)

toffee = require 'toffee'

engine = new toffee.engine { verbose: false, prettyPrintErrors: false }

compileFile = (filename) ->
  (options) -> engine.runSync filename, options

# technically the above is 'compiling' in that its returning a function that's runnable
# on a string, although it does not read from the file system and convert to JS until
# needed; it also supports partials

# let's compile!

template = compileFile('foo.toffee')

# now we can use it! the first use will be slightly slow
console.log template {name:'Chris'}
console.log template {name:'Henry'}
console.log template {name:'Jennie'}
#etc.
malgorithms commented 10 years ago

btw, unlike many other templating engines, the toffee engine above will recognize when foo.toffee changes and recompile it as necessary; so your compileFile does not need to get re-called. Again, if this is not the desired behavior you can just use views, and define your own partial fn.

malgorithms commented 10 years ago

async always, and it should return a compiled page

you mean call back with a compiled page?

malgorithms commented 10 years ago

Here's an entirely async version of compileFile (using it is async, and the actual run call is async):

toffee = require 'toffee'

engine = new toffee.engine { verbose: false, prettyPrintErrors: true }

compileFile = (filename, compile_cb) ->
  compile_cb (options, render_cb) -> engine.run filename, options, render_cb

# let's compile!

compileFile 'foo.toffee', (template) ->
  template {name:'Chris'},  (err, res) -> console.log [err, res]
  template {name:'Henry'},  (err, res) -> console.log [err, res]
  template {name:'Jennie'}, (err, res) -> console.log [err, res]
hhsnopek commented 10 years ago

I need a it to return a function, sorry

hhsnopek commented 10 years ago

toffee does support compiling client-side js templates right? It's not specified within the docs

hhsnopek commented 10 years ago

@malgorithms alright so I need a function returned from compiling. Right now compiling is making html when I should be getting js.

compileFile = (file) ->
  toffee.compile fs.readFileSync file

console.log compileFile('supplies.toffee')(options)

file:

{#
  for supply in supplies {:<li>#{supply}</li>:}
#}

result: <li>mop</li><li>trash bin</li><li>flashlight</li>

what I need is something like this:

(function () {
  var result = "";
  for(var x = 1; 1 < supplies.length; x++) {
    result.append("<li>" + supplies[ x ] + "</li>");
  }
  return result;
})();
malgorithms commented 10 years ago

what I need is something like this:

(function () {
  var result = "";
  for(var x = 1; 1 < supplies.length; x++) {
    result.append("<li>" + supplies[ x ] + "</li>");
  }
  return result;
})();

You're asking for a compile function that returns a function, which in turn returns a string based on var inputs. (note this is not async, even though you said you wanted async earlier.) Anyway, this is precisely what my example above does:

toffee = require 'toffee'

engine = new toffee.engine { verbose: false, prettyPrintErrors: false }

compileFile = (filename) ->
  (options) -> engine.runSync filename, options

see, you can use it like this:

template = compileFile('foo.toffee')

# template is such a function, just like what you wanted.

# now we can use it! the first use will be slightly slow
console.log template {name:'Chris'}

You're saying that you didn't want to get back the rendered output <li>mop</li><li>trash bin</li><li>flashlight</li>, but the reason you did is because you ended up calling the function with options:

console.log compileFile('supplies.toffee')(options)

But compileFile('supplies.toffee') is the function you're asking for. It returns a function that takes a string and renders it.

Is there anything missing? If you could be more specific that would be cool. Note this returns a [err, res] pair, so if you just want to compile a function that returns only res:

compileFile = (filename) ->
  (options) -> engine.runSync(filename, options)[1]
hhsnopek commented 10 years ago

ahhh ok, I totally didn't realize that before. I now understand! :smile: Also I figured out today that the file is already reads and passes a string(without me having to open and read the contents)

My result is null when I pass a string

hhsnopek commented 10 years ago

I got compiling to work! Thank you so much for your help. I learned a lot from this

malgorithms commented 10 years ago

excellent! Glad I could help.