Last active
June 8, 2022 08:15
-
-
Save morrelinko/84b391964914dd3e7761d62629eb029c to your computer and use it in GitHub Desktop.
Nunjucks Components
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
const { nodes, runtime } = require('nunjucks') | |
module.exports = function compileComponent (node, frame) { | |
let componentId = this._tmpid() | |
let templateId = this._tmpid() | |
let templateId2 = this._tmpid() | |
let componentVar = `component_${componentId}` | |
this._emitLine(`var ${componentVar} = {};`) | |
node.slots.forEach(slot => { | |
let slotName = `${componentVar}_slot_${slot.name.value}` | |
this._emitLine(`function ${slotName}(frame) {`) | |
this._emitLine(`var lineno = null;`) | |
this._emitLine(`var colno = null;`) | |
this._emitLine(`var ${this.buffer} = "";`) | |
this._emitLine('var frame = frame.push(true);') | |
let slotFrame = new runtime.Frame() | |
if (slot.body instanceof nodes.Literal) { | |
this._emitLine(`${this.buffer} = '${slot.body.value}';`) | |
} else { | |
this.compile(slot.body, slotFrame) | |
} | |
this._emitLine(`return new runtime.SafeString(${this.buffer});`) | |
this._emitLine('}') | |
this._emitLine(`${componentVar}.${slot.name.value} = ${slotName}(frame);`) | |
}) | |
this._emitLine('var tasks = [];') | |
this._emitLine('tasks.push(function (callback) {') | |
this._emit('env.getTemplate(') | |
this._compileExpression(node.template, frame) | |
this._emit( | |
`, true, ${this._templateName()}, false, ${this._makeCallback(templateId)}` | |
) | |
this._emitLine(`callback(null, ${templateId});`) | |
this._emitLine('});') | |
this._emitLine('});') | |
this._emitLine('tasks.push(function (template, callback) {') | |
this._emitLine(` | |
template.render( | |
Object.assign( | |
context.getVariables(), | |
${componentVar} | |
), | |
frame, | |
${this._makeCallback(templateId2)} | |
`) | |
this._emitLine(`callback(null, ${templateId2});`) | |
this._emitLine('});') | |
this._emitLine('});') | |
this._emitLine('tasks.push(function (result, callback) {') | |
this._emitLine(`${this.buffer} += result;`) | |
this._emitLine('callback();') | |
this._emitLine('});') | |
this._emitLine('env.waterfall(tasks, function () {') | |
this._addScopeLevel() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
nodes.Component = nodes.Node.extend('Component', { | |
fields: ['template', 'slots'] | |
}) | |
nodes.Slot = nodes.Node.extend('Slot', { | |
fields: ['name', 'body'] | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict' | |
const flatten = require('lodash/flatten') | |
const first = require('lodash/first') | |
class ComponentExtension { | |
get tags () { | |
return ['component', 'slot'] | |
} | |
parse (parser, nodes, lexer) { | |
let tag = parser.peekToken() | |
if ( | |
!parser.skipSymbol('component') && | |
!parser.skipSymbol('slot') && | |
!parser.skipSymbol('endslot') | |
) { | |
this.fail( | |
'ComponentExtension: expected "component" or "slot"', | |
tag.lineno, | |
tag.colno | |
) | |
} | |
// parse the component file | |
let template = parser.parseExpression() | |
let content = new nodes.NodeList(0, 0) | |
let slots = [] | |
let args = [] | |
// Compile inline arguments as slots | |
if ((args = parser.parseSignature(null, true))) { | |
// Pass inline args as slots | |
if (args.children) { | |
args = first(args.children) | |
} | |
if (args && args.children) { | |
args = args.children | |
} | |
if (args) { | |
args.forEach(arg => { | |
slots.push(new nodes.Slot( | |
arg.lineno, | |
arg.colno, | |
arg.key, | |
new nodes.Literal( | |
arg.lineno, | |
arg.colno, | |
arg.value.value | |
))) | |
}) | |
} | |
} | |
// advance until we visit a 'slot' or 'endcomponent' | |
parser.advanceAfterBlockEnd('component') | |
mergeChildren( | |
content, | |
parser.parseUntilBlocks('slot', 'endslot', 'endcomponent') | |
) | |
let tok = parser.peekToken() | |
while (tok && tok.value === 'slot') { | |
// skip the slot symbol and get the expression if any | |
parser.skipSymbol('slot') | |
let name = parser.parsePrimary() | |
let valueToken = parser.peekToken() | |
let slot = new nodes.Slot(tok.lineno, tok.colno, name) | |
if ( | |
valueToken.type !== lexer.TOKEN_STRING && | |
valueToken.type !== lexer.TOKEN_SYMBOL | |
) { | |
parser.skip(valueToken.type) | |
} | |
// get the body of the slot and add to slot list | |
if (valueToken.type === lexer.TOKEN_STRING) { | |
// case: inline slot | |
slot.body = new nodes.Literal( | |
valueToken.lineno, | |
valueToken.colno, | |
parser.parseExpression().value | |
) | |
parser.advanceAfterBlockEnd('component') | |
} else { | |
// case: block slot | |
slot.body = parser.parseUntilBlocks('endslot') | |
parser.advanceAfterBlockEnd() | |
} | |
mergeChildren( | |
content, | |
parser.parseUntilBlocks('slot', 'endslot', 'endcomponent') | |
) | |
slots.push(slot) | |
// Move on to next slot | |
tok = parser.peekToken() | |
} | |
// Remove 'content' arg entirely | |
// if no data is available | |
if (content.children.length > 0) { | |
// create content slot | |
slots.push( | |
new nodes.Slot( | |
tag.lineno, | |
tag.colno, | |
new nodes.Value(0, 0, 'content'), | |
content | |
) | |
) | |
} | |
switch (tok.value) { | |
case 'endcomponent': | |
parser.advanceAfterBlockEnd() | |
break | |
} | |
return new nodes.Component(tag.lineno, tag.colno, template, slots) | |
} | |
} | |
function mergeChildren (targetList, sourceList) { | |
let children = flatten(sourceList.children) | |
children.forEach(child => { | |
targetList.addChild(child) | |
}) | |
} | |
module.exports = ComponentExtension |
Trouble with nested components (SyntaxError: missing ) after argument list
):
{% component "some-component.njk" prop="value1" %}
{% component "another-component.njk" prop="value2" %}
{% endcomponent %}
{% endcomponent %}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Easily build reusable components on nunjucks if you are able to put the pieces of code above together...
WHY ??
Here's Why!!!
Usage
inline without slots
{% component 'views/components/forms.njk' %}
with slots & inline slots, and argument slots
slots are just variables passed as context to the rendered template as shown below...
slots can be passed as an expression or as ... just see the example...
{# Template views/components/forms.njk #}