Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x-for patch #151

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/alpine.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/alpine.js.map

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,17 +253,19 @@ export default class Component {
}

evaluateReturnExpression(el, expression, extraVars = () => {}) {
return saferEval(expression, this.$data, {
...extraVars(),
$dispatch: this.getDispatchFunction(el),
})
let extraData = extraVars()
if (typeof extraData === 'undefined') extraData = {}
extraData['$dispatch'] = this.getDispatchFunction(el)

return saferEval(expression, this.$data, extraData)
}

evaluateCommandExpression(el, expression, extraVars = () => {}) {
saferEvalNoReturn(expression, this.$data, {
...extraVars(),
$dispatch: this.getDispatchFunction(el),
})
let extraData = extraVars()
if (typeof extraData === 'undefined') extraData = {}
extraData['$dispatch'] = this.getDispatchFunction(el)

saferEvalNoReturn(expression, this.$data, extraData)
}

getDispatchFunction (el) {
Expand Down
34 changes: 32 additions & 2 deletions src/directives/for.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,23 @@ export function handleForDirective(component, el, expression, initialUpdate) {

currentEl.__x_for_alias = single
currentEl.__x_for_value = i

component.updateElements(currentEl, () => {
return {[currentEl.__x_for_alias]: currentEl.__x_for_value}
// We wrap the additional variables in a proxy
// so we will update the original variable
// if somebody reassign it from the item context
// or bind it to a model
return new Proxy(
{ [currentEl.__x_for_alias]: currentEl.__x_for_value },
{
set(obj, property, value) {
if (property === currentEl.__x_for_alias) {
return Reflect.set(group, index, value)
}
return Reflect.set(obj, property, value)
}
}
)
})
} else {
// There are no more .__x_for_key elements, meaning the page is first loading, OR, there are
Expand All @@ -60,8 +75,23 @@ export function handleForDirective(component, el, expression, initialUpdate) {
// always up to date for listener handlers that don't get re-registered.
currentEl.__x_for_alias = single
currentEl.__x_for_value = i

component.initializeElements(currentEl, () => {
return {[currentEl.__x_for_alias]: currentEl.__x_for_value}
// We wrap the additional variables in a proxy
// so we will update the original variable
// if somebody reassign it from the item context
// or bind it to a model
return new Proxy(
{[currentEl.__x_for_alias]: currentEl.__x_for_value},
{
set(obj, property, value) {
if (property === currentEl.__x_for_alias) {
return Reflect.set(group, index, value)
}
return Reflect.set(obj, property, value)
}
}
)
})
}

Expand Down
9 changes: 5 additions & 4 deletions src/directives/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ export function registerModelListener(component, el, modifiers, expression, extr
const listenerExpression = `${expression} = rightSideOfExpression($event, ${expression})`

registerListener(component, el, event, modifiers, listenerExpression, () => {
return {
...extraVars(),
rightSideOfExpression: generateModelAssignmentFunction(el, modifiers, expression),
}
let extraData = extraVars()
if (typeof extraData === 'undefined') extraData = {}
extraData['rightSideOfExpression'] = generateModelAssignmentFunction(el, modifiers, expression)

return extraData
})
}

Expand Down
6 changes: 5 additions & 1 deletion src/directives/on.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export function registerListener(component, el, event, modifiers, expression, ex

function runListenerHandler(component, expression, e, extraVars) {
component.evaluateCommandExpression(e.target, expression, () => {
return {...extraVars(), '$event': e}
let extraData = extraVars()
if (typeof extraData === 'undefined') extraData = {}
extraData.$event = e

return extraData
})
}

Expand Down
14 changes: 10 additions & 4 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@ export function debounce(func, wait) {
}

export function saferEval(expression, dataContext, additionalHelperVariables = {}) {
return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `var result; with($data) { result = ${expression} }; return result`))(
dataContext, ...Object.values(additionalHelperVariables)
return (new Function(
['$data', 'additionalHelperVariables'],
`var result; with (additionalHelperVariables) { with($data) { result = ${expression} } }; return result`)
)(
dataContext, additionalHelperVariables
)
}

export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
return (new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { ${expression} }`))(
dataContext, ...Object.values(additionalHelperVariables)
return (new Function(
['dataContext', 'additionalHelperVariables'],
`with (additionalHelperVariables) { with(dataContext) { ${expression} } }`
))(
dataContext, additionalHelperVariables
)
}

Expand Down
52 changes: 50 additions & 2 deletions test/for.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Alpine from 'alpinejs'
import { wait } from '@testing-library/dom'
import { wait, fireEvent } from '@testing-library/dom'

global.MutationObserver = class {
observe() {}
Expand Down Expand Up @@ -69,7 +69,7 @@ test('removes all elements when array is empty and previously had multiple items
await wait(() => { expect(document.querySelectorAll('span').length).toEqual(0) })
})

test('elements inside of loop are reactive', async () => {
test('elements inside of loop are reactive - external changes', async () => {
document.body.innerHTML = `
<div x-data="{ items: ['first'], foo: 'bar' }">
<button x-on:click="foo = 'baz'"></button>
Expand Down Expand Up @@ -97,6 +97,54 @@ test('elements inside of loop are reactive', async () => {
})
})

test('elements inside of loop are reactive - internal changes', async () => {
document.body.innerHTML = `
<div x-data="{ items: ['first', 'second'] }">

<template x-for="item in items">
<div>
<span x-text="item"></span>
<button x-on:click="item = 'foo'"></button>
</div>
</template>
</div>
`

Alpine.start()

expect(document.querySelectorAll('span')[0].innerText).toEqual('first')

document.querySelectorAll('button')[0].click()

await wait(() => {
expect(document.querySelectorAll('span')[0].innerText).toEqual('foo')
})
})

test('elements can be bound to models', async () => {
document.body.innerHTML = `
<div x-data="{ items: ['first', 'second'] }">

<template x-for="item in items">
<div>
<span x-text="item"></span>
<input x-model="item" />
</div>
</template>
</div>
`

Alpine.start()

expect(document.querySelectorAll('span')[0].innerText).toEqual('first')

fireEvent.input(document.querySelectorAll('input')[0], { target: { value: 'foo' }})

await wait(() => {
expect(document.querySelectorAll('span')[0].innerText).toEqual('foo')
})
})

test('components inside of loop are reactive', async () => {
document.body.innerHTML = `
<div x-data="{ items: ['first'] }">
Expand Down