Skip to content

Commit

Permalink
feat(each): add support for inlcude inside each
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Mar 27, 2017
1 parent 6ebc45a commit c876c2d
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/Tags/ComponentTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class ComponentTag extends BaseTag {
return { name: statement.toStatement(), props: [] }
}

const [firstChild, ...props] = statement.toObject()
const [firstChild, ...props] = statement.toStatement()
const name = lexer.parseRaw(firstChild).toStatement()
return { name, props }
}
Expand Down
73 changes: 70 additions & 3 deletions src/Tags/EachTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,70 @@ class EachTag extends BaseTag {
* @return {Array}
*/
get allowedExpressions () {
return ['BinaryExpression']
return ['BinaryExpression', 'SequenceExpression']
}

/**
* Returns the partial to be include for each loop,
* only if defined
*
* @method _getPartial
*
* @param {Object} expression
*
* @return {String|Null}
*/
_getPartial (expression) {
if (!expression.toObject) {
return null
}

let keyValuePairs = expression.toObject()

/**
* Find the object where key is include if keyValuePairs
* is an array
*/
if (_.isArray(keyValuePairs)) {
keyValuePairs = _.find(keyValuePairs, (item) => item.key === 'include')
}
return (keyValuePairs.key && keyValuePairs.key === 'include') ? keyValuePairs.value : null
}

/**
* Returns the each statement and the include template, by
* parsing each allowed expression differently.
*
* @method _getCompiledAndIncludeStatement
*
* @param {Object} lexer
* @param {Object} body
* @param {Number} lineno
*
* @return {Array}
* @throws {Error} If first member of the sequence is not a binary expression.
*/
_getCompiledAndIncludeStatement (lexer, body, lineno) {
const preCompiledStatement = this._compileStatement(lexer, body, lineno)
/**
* If statement is not sequence return it as the only statement
* and include partial is set to null.
*/
if (preCompiledStatement.type !== 'sequence') {
return [preCompiledStatement, null]
}

const compiledStatement = preCompiledStatement.tokens.members.shift()
/**
* Throw exception if the first token on the sequence expression
* is not a binary expression, since that is what we need.
*/
if (compiledStatement.type !== 'binary') {
throw CE.InvalidExpressionException.invalidTagExpression(body, this.tagName, lineno, '1')
}

const includeStatement = this._getPartial(preCompiledStatement.tokens.members[0])
return [compiledStatement, includeStatement]
}

/**
Expand All @@ -72,7 +135,7 @@ class EachTag extends BaseTag {
* @return {void}
*/
compile (compiler, lexer, buffer, { body, childs, lineno }) {
const compiledStatement = this._compileStatement(lexer, body, lineno)
const [compiledStatement, includeStatement] = this._getCompiledAndIncludeStatement(lexer, body, lineno)

/**
* Throw exception when invalid operator is used.
Expand Down Expand Up @@ -155,7 +218,11 @@ class EachTag extends BaseTag {
/**
* Parse all childs
*/
childs.forEach((child) => compiler.parseLine(child))
if (includeStatement) {
buffer.writeToOutput(`$\{${lexer.runTimeRenderFn}(${includeStatement})}`, false)
} else {
childs.forEach((child) => compiler.parseLine(child))
}

/**
* Close the each loop
Expand Down
1 change: 1 addition & 0 deletions test-helpers/views/includes/users-loop.edge
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>{{ user.username }}</p>
1 change: 1 addition & 0 deletions test-helpers/views/includes/veggie.edge
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<li> {{ name }} has {{ calories }} calories </li>
10 changes: 5 additions & 5 deletions test/unit/lexer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,21 @@ test.group('Lexer', (group) => {
assert.equal(parsedStatement.toStatement(), `this.context.accessChild(this.context.resolve('users'), [this.context.resolve('username')]) === this.context.accessChild(this.context.resolve('deletedUsers'), [this.context.resolve('username')])`)
})

test('parse sequence expression to object', (assert) => {
test('parse sequence expression to statement', (assert) => {
const statement = `'message', { message }`
const parsedObject = this.lexer.parseRaw(statement).toObject()
const parsedObject = this.lexer.parseRaw(statement).toStatement()
assert.deepEqual(parsedObject, [`'message'`, `{message: this.context.resolve('message')}`])
})

test('parse sequence expression with assignment to object', (assert) => {
test('parse sequence expression with assignment to statement', (assert) => {
const statement = `'message', from = user, isPrimary = true`
const parsedObject = this.lexer.parseRaw(statement).toObject()
const parsedObject = this.lexer.parseRaw(statement).toStatement()
assert.deepEqual(parsedObject, [`'message'`, `{from: this.context.resolve('user')}`, `{isPrimary: true}`])
})

test('parse sequence expression with assignment and object literal', (assert) => {
const statement = `'message', from = user, { message: message }`
const parsedObject = this.lexer.parseRaw(statement).toObject()
const parsedObject = this.lexer.parseRaw(statement).toStatement()
assert.deepEqual(parsedObject, [`'message'`, `{from: this.context.resolve('user')}`, `{message: this.context.resolve('message')}`])
})

Expand Down
124 changes: 122 additions & 2 deletions test/unit/tags/each.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

const _ = require('lodash')
const test = require('japa')
const path = require('path')
const Template = require('../../../src/Template')
const TemplateRunner = require('../../../src/Template/Runner')
const Context = require('../../../src/Context')
const Loader = require('../../../src/Loader')
const dedent = require('dedent-js')
const loader = new Loader(path.join(__dirname, '../../../test-helpers/views'))

test.group('Tags | Each ', (group) => {
group.before(() => {
Expand Down Expand Up @@ -75,12 +78,12 @@ test.group('Tags | Each ', (group) => {

test('throw exception when expression is not binary', (assert) => {
const statement = dedent`
@each(user, users)
@each(user.users)
@endeach
`
const template = new Template(this.tags)
const output = () => template.compileString(statement)
assert.throw(output, 'lineno:1 charno:0 E_INVALID_EXPRESSION: Invalid expression <user, users> passed to (each) block')
assert.throw(output, 'lineno:1 charno:0 E_INVALID_EXPRESSION: Invalid expression <user.users> passed to (each) block')
})

test('throw exception when expression operator is not {in}', (assert) => {
Expand Down Expand Up @@ -270,4 +273,121 @@ test.group('Tags | Each ', (group) => {
nikk
`)
})

test('include partial within each block', (assert) => {
const statement = dedent`
@!each(user in users, include = 'includes.users-loop')
`
const templateInstance = new Template(this.tags, {}, {}, loader)
const template = templateInstance.compileString(statement)
this.tags.each.run(Context)

const ctx = new Context('', {
users: [{username: 'virk'}, {username: 'nikk'}]
})
templateInstance.context = ctx
const output = new TemplateRunner(template, templateInstance).run()
assert.equal(output.trim(), dedent`
<p>virk</p>
<p>nikk</p>
`)
})

test('include dynamic partial within each block', (assert) => {
const statement = dedent`
@!each(user in users, include = usersTmp)
`
const templateInstance = new Template(this.tags, {}, {}, loader)
const template = templateInstance.compileString(statement)
this.tags.each.run(Context)

const ctx = new Context('', {
users: [{username: 'virk'}, {username: 'nikk'}],
usersTmp: 'includes.users-loop'
})
templateInstance.context = ctx
const output = new TemplateRunner(template, templateInstance).run()
assert.equal(output.trim(), dedent`
<p>virk</p>
<p>nikk</p>
`)
})

test('include partial with fallback else inside each block', (assert) => {
const statement = dedent`
@each(user in users, include = 'includes.users-loop')
@else
<h2> No users found </h2>
@endeach
`
const templateInstance = new Template(this.tags, {}, {}, loader)
const template = templateInstance.compileString(statement)
this.tags.each.run(Context)

const ctx = new Context('', {
users: []
})
templateInstance.context = ctx
const output = new TemplateRunner(template, templateInstance).run()
assert.equal(output.trim(), dedent`
<h2> No users found </h2>
`)
})

test('loop over key value and include partial inside each loop', (assert) => {
const statement = dedent`
@!each((calories, name) in veggies, include = 'includes.veggie')
`
const templateInstance = new Template(this.tags, {}, {}, loader)
const template = templateInstance.compileString(statement)
this.tags.each.run(Context)

const ctx = new Context('', {
veggies: {
tomato: '18',
potato: '77',
carrot: '41'
}
})
templateInstance.context = ctx
const output = new TemplateRunner(template, templateInstance).run()
assert.equal(output.trim(), dedent`
<li> tomato has 18 calories </li>
<li> potato has 77 calories </li>
<li> carrot has 41 calories </li>
`)
})

test('work with include is defined as a object', (assert) => {
const statement = dedent`
@!each((calories, name) in veggies, { include: 'includes.veggie' })
`
const templateInstance = new Template(this.tags, {}, {}, loader)
const template = templateInstance.compileString(statement)
this.tags.each.run(Context)

const ctx = new Context('', {
veggies: {
tomato: '18',
potato: '77',
carrot: '41'
}
})
templateInstance.context = ctx
const output = new TemplateRunner(template, templateInstance).run()
assert.equal(output.trim(), dedent`
<li> tomato has 18 calories </li>
<li> potato has 77 calories </li>
<li> carrot has 41 calories </li>
`)
})

test('throw when include expression is passed along with include statement', (assert) => {
const statement = dedent`
@!each(calories, name in veggies, { include: 'includes.veggie' })
`
const templateInstance = new Template(this.tags, {}, {}, loader)
const template = () => templateInstance.compileString(statement)
assert.throw(template, `lineno:1 charno:1 E_INVALID_EXPRESSION: Invalid expression <calories, name in veggies, { include: 'includes.veggie' }> passed to (each) block`)
})
})

0 comments on commit c876c2d

Please sign in to comment.