1 | rincewind
|
2 | ===
|
3 |
|
4 | An HTML based template engine with a few ever-so-slightly magic attributes.
|
5 |
|
6 | [![NPM](https://nodei.co/npm/rincewind.png?compact=true)](https://nodei.co/npm/rincewind/)
|
7 |
|
8 | ## Example
|
9 |
|
10 | Here's some data we want to display to the user. It's a basic blog:
|
11 |
|
12 | ```js
|
13 | var data = {
|
14 |
|
15 | post: {
|
16 | title: "Rincewind! The not really but sort of magic templating engine.",
|
17 | date: "1 April 2013",
|
18 | body: "A lot of text with clever puns etc."
|
19 | },
|
20 |
|
21 | comments: [
|
22 | { name: 'Joe Blogs',
|
23 | date: '2 April 2013',
|
24 | body: "Get it? My name is Joe and a blog ;)"
|
25 | },
|
26 | { name: 'Anonymous Coward',
|
27 | date: '2 April 2013',
|
28 | body: "I am a coward."
|
29 | }
|
30 | ]
|
31 |
|
32 | }
|
33 | ```
|
34 |
|
35 | And here's a view to make it browsery:
|
36 |
|
37 | ```html
|
38 | <!-- post.html -->
|
39 | <? require './markdown.js' as markdown ?>
|
40 | <? require './widget.html' as widget ?>
|
41 |
|
42 | <article class='Post'>
|
43 | <header>
|
44 | <h1 t:bind='post.title' />
|
45 | <p t:bind='post.date' />
|
46 | </header>
|
47 | <div t:bind='post.body' t:view='markdown' />
|
48 |
|
49 | <section class='comments'>
|
50 | <header>Comments</header>
|
51 |
|
52 | <div t:repeat='comments'>
|
53 | <header>
|
54 | <strong t:bind='.name' /> on
|
55 | <span t:bind='.date' />
|
56 | </header>
|
57 | <div t:bind='.body' t:view='markdown' />
|
58 | </div>
|
59 |
|
60 | </section>
|
61 |
|
62 | <aside t:view='widget' />
|
63 | </article>
|
64 | ```
|
65 |
|
66 | ```html
|
67 | <!-- widget.html -->
|
68 | <h1>Cool Links!</h1>
|
69 | <ul>
|
70 | <li><a href='https://github.com'>Github</a></li>
|
71 | <li><a href='http://nodejs.org'>Node.js</a></li>
|
72 | <li><a href='https://npmjs.org'>npm</a></li>
|
73 | </ul>
|
74 | ```
|
75 |
|
76 | ```js
|
77 | // markdown.js
|
78 | var marked = require('marked')
|
79 | module.exports = function(context){
|
80 | return marked(context.source)
|
81 | }
|
82 | ```
|
83 |
|
84 | And a master/layout to hold it:
|
85 |
|
86 | ```html
|
87 | <!-- master.html -->
|
88 | <html>
|
89 | <head>
|
90 | <title>Matt's Blog</title>
|
91 | <link rel='stylesheet' href='/styles.css' />
|
92 | </head>
|
93 | <body>
|
94 |
|
95 | <header>
|
96 | <h1><a href='/'>Matt's Blog</a></h1>
|
97 | <p>Not just another wordpress</p>
|
98 | </header>
|
99 |
|
100 | <div>
|
101 | <t:placeholder t:content />
|
102 | </div>
|
103 |
|
104 | <footer>Powered by Node, NPM and You</footer>
|
105 | </body>
|
106 |
|
107 | </html>
|
108 | ```
|
109 |
|
110 | ### Now let's hook it all up!
|
111 |
|
112 | Create our views and do some databinding with json-query.
|
113 |
|
114 | ```js
|
115 | var fs = require('fs')
|
116 | var View = require('rincewind')
|
117 | var jsonQuery = require('json-query')
|
118 |
|
119 | var master = View(__dirname + '/master.html')
|
120 | var renderView = View(__dirname + '/post.html')
|
121 |
|
122 | function respond(req, res, data){
|
123 | var queryHandler = function(query){
|
124 | return jsonQuery(query, {
|
125 | rootContext: data,
|
126 | context: this.source,
|
127 | override: this.override
|
128 | }).value
|
129 | }
|
130 |
|
131 | var html = master({
|
132 | get: queryHandler
|
133 | content: renderView({get: queryHandler})
|
134 | })
|
135 |
|
136 | res.end(html)
|
137 | }
|
138 |
|
139 | var server = http.createServer(function(req,res){
|
140 | respond(req, res, data)
|
141 | })
|
142 | ```
|
143 |
|
144 | ### Inline views
|
145 |
|
146 | ```js
|
147 | var View = require('rincewind')
|
148 | var renderView = View({parse: '<div><div t:bind="value" /></div>'})
|
149 | ```
|
150 |
|
151 | ## Preloading views
|
152 |
|
153 | ### Manually
|
154 |
|
155 | ```js
|
156 | var View = require('rincewind')
|
157 | var precompiled = View(__dirname + '/view.html').getCompiledView()
|
158 | ```
|
159 |
|
160 | Then send the precompiled value to the browser.
|
161 |
|
162 | ```js
|
163 | // recreate in the browser
|
164 | var View = require('rincewind')
|
165 | var renderView = View(precompiled)
|
166 | ```
|
167 |
|
168 | ### Browserify transform
|
169 |
|
170 | Or you can use [rincewind-precompile-transform](https://github.com/mmckegg/rincewind-precompile-transform) which will automatically compile and inline any rincewind views in your source for the browser bundle.
|
171 |
|
172 | ```bash
|
173 | $ browserify entry.js -t rincewind-precompile-transform > output.js
|
174 | ```
|
175 |
|
176 | ### Watch script
|
177 |
|
178 | For more control use [rincewind-watch](https://github.com/mmckegg/rincewind-watch) to and trigger callbacks on changes and manually compile to javascript (and call `view.stringify(relativePath)`). The compiled file can then be required by your code.
|
179 |
|
180 |
|
181 | ## The magic t:attributes
|
182 |
|
183 |
|
184 | ### Attribute: `t:bind`
|
185 |
|
186 | Any time the system hits a `t:bind` attribute while rendering the view, it calls the `queryHandler` function with the value and additional context info (`parent`, `source` etc).
|
187 |
|
188 | ### Attribute: `t:bind:<attribute-name>`
|
189 |
|
190 | We can bind arbitrary attributes using the same method by using `t:bind:<attribute-name>`.
|
191 |
|
192 | For example, if we wanted to bind an element's ID to the query `element_id`:
|
193 |
|
194 | ```html
|
195 | <span t:bind:id='element_id'>content unchanged</span>
|
196 | ```
|
197 |
|
198 | Which would output:
|
199 |
|
200 | ```html
|
201 | <span id='value'>content unchanged</span>
|
202 | ```
|
203 |
|
204 | ### Attribute: `t:if`
|
205 |
|
206 | The element will only be rendered if the specified query **returns a truthy value**.
|
207 |
|
208 | ```html
|
209 | <span t:if='show_content'>Only shows if show_content returns true</span>
|
210 | ```
|
211 |
|
212 | ### Attribute: `t:unless`
|
213 |
|
214 | The inverse of `t:if`. The element will only be rendered if the specified query **returns a falsy value**.
|
215 |
|
216 | ### Attribute: `t:by` and `t:when`
|
217 |
|
218 | An extension of the if system. Much like a `switch` or `case` statement. Specify the source query using `t:by` then any sub-elements can use `t:when` to choose what value the `t:by` query must return in order for them to show. Multiple values may be specified by separating with the pipe symbol (e.g. `value1|value2|value3`).
|
219 |
|
220 | ```html
|
221 | <div t:by='type'>
|
222 | <div t:when='example'>
|
223 | This div is only rendered if the query "type" returns the value "example".
|
224 | </div>
|
225 | <div t:when='production'>
|
226 | This div is only rendered if the query "type" returns the value "production".
|
227 | </div>
|
228 | <div t:when='trick|treat'>
|
229 | This div is rendered when the query "type" returns the value "trick" or "treat".
|
230 | </div>
|
231 | </div>
|
232 | ```
|
233 |
|
234 | ### Attribute: `t:repeat`
|
235 |
|
236 | For binding to arrays and creating repeating content. The attribute value is queried and the element is duplicated for every item in the returned array.
|
237 |
|
238 | For this [JSON Context](http://github.com/mmckegg/json-context) datasource:
|
239 |
|
240 | ```js
|
241 | var datasource = JsonContext({
|
242 | posts: [
|
243 | {id: 1, title: "Post 1", body: "Here is the body content"},
|
244 | {id: 2, title: "Post 2", body: "Here is some more body content"},
|
245 | {id: 3, title: "Post 3", body: "We're done."},
|
246 | ]
|
247 | })
|
248 | ```
|
249 |
|
250 | And this template:
|
251 |
|
252 | ```html
|
253 | <div class='post' t:repeat='posts' t:bind:data-id='.id'>
|
254 | <h1 t:bind='.title'>Will replaced with the value of title</h1>
|
255 | <div t:bind='.body'>Will replaced with the value of body</div>
|
256 | </div>
|
257 | ```
|
258 |
|
259 | We would get:
|
260 |
|
261 | ```html
|
262 | <div class='post' data-id='1'>
|
263 | <h1>Post 1</h1>
|
264 | <div>Here is the body content</div>
|
265 | </div>
|
266 | <div class='post' data-id='2'>
|
267 | <h1>Post 2</h1>
|
268 | <div>Here is some more body content</div>
|
269 | </div>
|
270 | <div class='post' data-id='3'>
|
271 | <h1>Post 3</h1>
|
272 | <div>We're done.</div>
|
273 | </div>
|
274 | ```
|
275 |
|
276 | If required (e.g. nesting repeaters) you can use `t:as` to assign the context a name and reference it by that instead of '.' - this will only work if `templateContext.override` is handled by the query engine.
|
277 |
|
278 | ```html
|
279 | <div class='post' t:repeat='posts' t:as='post' t:bind:data-id='.id'>
|
280 | <div t:repeat='something_else'>
|
281 | Can still access the post!
|
282 | <span t:bind='post.name' />
|
283 | </div>
|
284 | </div>
|
285 | ```
|
286 | ### Attribute: `t:view`
|
287 |
|
288 | Specify a sub-view to render as the content of the element.
|
289 |
|
290 | ```html
|
291 | <!-- view.html -->
|
292 | <? require './subview.html' as subview ?>
|
293 | <div>
|
294 | <div t:view='subview' />
|
295 | </div>
|
296 | ```
|
297 |
|
298 | ```html
|
299 | <!--- subview.html -->
|
300 | <div>Sub-view content</div>
|
301 | ```
|
302 |
|
303 | Format content in specific way:
|
304 |
|
305 | ```html
|
306 | <? require './format.html' as format ?>
|
307 | <div>
|
308 | <div t:bind='contact' t:view='format' />
|
309 | </div>
|
310 | ```
|
311 |
|
312 | ```html
|
313 | <!--- format.html -->
|
314 | <strong><span t:bind='.name' />:</strong> <span t:bind='.address' />
|
315 | ```
|
316 |
|
317 | Or require javascript view:
|
318 |
|
319 | ```html
|
320 | <? require './markdown.html' as markdown ?>
|
321 | <div>
|
322 | <div t:bind='body' t:view='markdown' />
|
323 | </div>
|
324 | ```
|
325 |
|
326 | ```js
|
327 | // markdown.js
|
328 | var marked = require('marked')
|
329 | module.exports = function(context){
|
330 | return marked(context.source)
|
331 | }
|
332 | ```
|
333 |
|
334 | Wrap content using javascript:
|
335 |
|
336 | ```html
|
337 | <? require './wrapper.html' as wrap ?>
|
338 | <div>
|
339 | <t:placeholder t:view='wrap' />
|
340 | </div>
|
341 | ```
|
342 |
|
343 | ```js
|
344 | // wrapper.html
|
345 | var marked = require('marked')
|
346 | module.exports = function(context){
|
347 | return '<strong>' + context.content + '</strong>'
|
348 | }
|
349 | ```
|
350 |
|
351 | If the element had content specified, it will be overrided with the content of the subview, but if the subview contains an element with the attribute `t:content`, the removed content will be inserted here. This allows creating views that act like wrappers.
|
352 |
|
353 | ### Attribute: `t:content`
|
354 |
|
355 | This attribute accepts no value and is used to denote where to insert inner content.
|
356 |
|
357 | Say we have this master layout:
|
358 |
|
359 | ```html
|
360 | <!--/views/layout.master.html-->
|
361 | <html>
|
362 | <head>
|
363 | <title>My Blog</title>
|
364 | </head>
|
365 | <body>
|
366 | <h1>My Blog</h1>
|
367 | <div t:content id='content'></div>
|
368 | </body>
|
369 | </html>
|
370 | ```
|
371 |
|
372 | And this view:
|
373 |
|
374 | ```html
|
375 | <!--/views/content.html-->
|
376 | <h2>Page title</h2>
|
377 | <div>I am the page content</div>
|
378 | ```
|
379 |
|
380 | We would get:
|
381 |
|
382 | ```html
|
383 | <html>
|
384 | <head>
|
385 | <title>My Blog</title>
|
386 | </head>
|
387 | <body>
|
388 | <h1>My Blog</h1>
|
389 | <div id='content'> <!--inner view is inserted here-->
|
390 | <h2>Page title</h2>
|
391 | <div>I am the page content</div>
|
392 | </div>
|
393 | </body>
|
394 | </html>
|
395 | ```
|