Skip to content

Instantly share code, notes, and snippets.

@morrelinko
Last active June 8, 2022 08:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save morrelinko/84b391964914dd3e7761d62629eb029c to your computer and use it in GitHub Desktop.
Save morrelinko/84b391964914dd3e7761d62629eb029c to your computer and use it in GitHub Desktop.
Nunjucks Components
'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()
}
nodes.Component = nodes.Node.extend('Component', {
fields: ['template', 'slots']
})
nodes.Slot = nodes.Node.extend('Slot', {
fields: ['name', 'body']
})
'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
@morrelinko
Copy link
Author

morrelinko commented May 30, 2018

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...

{% component 'views/components/forms.njk'  name='email' %}
    {% slot prepend %}
        <div class="input-prepend">
    {% endslot %}

    {% slot append 'Appended Text' %}
{% endcomponent %}

{# Template views/components/forms.njk #}

<div class="form-group">
   {{ prepend | safe if prepend }}
   <h3>Yay! Some advance component stuff by {{ name }}</h3>
   {{ append | safe if append }}
</div>

@monochromer
Copy link

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