Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
calebporzio committed Dec 2, 2019
1 parent e8aabfc commit 47fddce
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 14 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Add the following script to the end of your `<head>` section.

## Learn

There are 6 directives available to you:
There are 7 directives available to you:

| Directive
| --- |
Expand All @@ -55,6 +55,7 @@ There are 6 directives available to you:
| `x-on` |
| `x-model` |
| `x-text` |
| `x-ref` |
| `x-cloak` |

Here's how they each work:
Expand Down Expand Up @@ -145,6 +146,17 @@ Adding `.stop` to an event listener will call `stopPropagation` on the triggered

---

### `x-ref`
**Example:** `<div x-ref="foo"></div><button x-on:click="$refs.foo.innerText = 'bar'"></button>`

**Structure:** `<div x-ref="[ref name]"></div><button x-on:click="$refs.[ref name].innerText = 'bar'"></button>`

`x-ref` provides a convenient way to retrieve raw DOM elements out of your component. By setting an `x-ref` attribute on an element, you are making it available to all event handlers inside an object called `$refs`.

This is a helpful alternative to setting ids and using `document.querySelector` all over the place.

---

### `x-cloak`
**Example:** `<div x-data="{}" x-cloak></div>`

Expand Down
4 changes: 2 additions & 2 deletions dist/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"/project-x.js": "/project-x.js?id=7551001734d762a47f88",
"/project-x.min.js": "/project-x.min.js?id=c8c54e0f29e2563e41d2"
"/project-x.js": "/project-x.js?id=bd450a04a07206020f63",
"/project-x.min.js": "/project-x.min.js?id=aca2ba4b017885bdd4a5"
}
31 changes: 27 additions & 4 deletions dist/project-x.js
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,8 @@ function () {
key: "runListenerHandler",
value: function runListenerHandler(expression, e) {
this.evaluateCommandExpression(expression, {
'$event': e
'$event': e,
'$refs': this.getRefsProxy()
});
}
}, {
Expand Down Expand Up @@ -1170,6 +1171,28 @@ function () {
option.selected = arrayWrappedValue.includes(option.value || option.text);
});
}
}, {
key: "getRefsProxy",
value: function getRefsProxy() {
var self = this; // One of the goals of this project is to not hold elements in memory, but rather re-evaluate
// the DOM when the system needs something from it. This way, the framework is flexible and
// friendly to outside DOM changes from libraries like Vue/Livewire.
// For this reason, I'm using an "on-demand" proxy to fake a "$refs" object.

return new Proxy({}, {
get: function get(object, property) {
var ref; // We can't just query the DOM because it's hard to filter out refs in
// nested components.

Object(_utils__WEBPACK_IMPORTED_MODULE_0__["walkSkippingNestedComponents"])(self.el, function (el) {
if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
ref = el;
}
});
return ref;
}
});
}
}]);

return Component;
Expand Down Expand Up @@ -1331,7 +1354,7 @@ function walkSkippingNestedComponents(el, callback) {

while (node) {
if (node.hasAttribute('x-data')) return;
walk(node, callback);
walkSkippingNestedComponents(node, callback);
node = node.nextElementSibling;
}
}
Expand Down Expand Up @@ -1365,12 +1388,12 @@ function saferEvalNoReturn(expression, dataContext) {
return new Function(['$data'].concat(_toConsumableArray(Object.keys(additionalHelperVariables))), "with($data) { ".concat(expression, " }")).apply(void 0, [dataContext].concat(_toConsumableArray(Object.values(additionalHelperVariables))));
}
function isXAttr(attr) {
var xAttrRE = /x-(on|bind|data|text|model|cloak)/;
var xAttrRE = /x-(on|bind|data|text|model|cloak|ref)/;
return xAttrRE.test(attr.name);
}
function getXAttrs(el, type) {
return Array.from(el.attributes).filter(isXAttr).map(function (attr) {
var typeMatch = attr.name.match(/x-(on|bind|data|text|model|cloak)/);
var typeMatch = attr.name.match(/x-(on|bind|data|text|model|cloak|ref)/);
var valueMatch = attr.name.match(/:([a-zA-Z\-]+)/);
var modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
return {
Expand Down
2 changes: 1 addition & 1 deletion dist/project-x.min.js

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@
[x-cloak] { display: none; }
</style>

<script src="https://cdn.jsdelivr.net/gh/calebporzio/project-x@v0.2.0/dist/project-x.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/gh/calebporzio/project-x@v0.3.0/dist/project-x.min.js" defer></script>
</head>
<body>
<div x-data="{ foo: 'bar' }">
<span x-text="foo"></span>

<div x-data="{ foo: 'bob' }">
<span x-ref="lob">hey</span>
<button x-on:click="console.log($refs.lob)">Something</button>
</div>
</div>

<div x-data="{ foo: 'bar' }">
<div x-on:click="foo = 'baz'">
<button x-on:click.stop></button>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"main": "dist/project-x.js",
"name": "project-x",
"version": "0.2.0",
"version": "0.3.0",
"scripts": {
"test": "jest",
"test:debug": "node --inspect node_modules/.bin/jest --runInBand",
Expand Down
29 changes: 28 additions & 1 deletion src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ export default class Component {
}

runListenerHandler(expression, e) {
this.evaluateCommandExpression(expression, { '$event': e })
this.evaluateCommandExpression(expression, {
'$event': e,
'$refs': this.getRefsProxy()
})
}

evaluateReturnExpression(expression) {
Expand Down Expand Up @@ -262,4 +265,28 @@ export default class Component {
option.selected = arrayWrappedValue.includes(option.value || option.text)
})
}

getRefsProxy() {
var self = this

// One of the goals of this project is to not hold elements in memory, but rather re-evaluate
// the DOM when the system needs something from it. This way, the framework is flexible and
// friendly to outside DOM changes from libraries like Vue/Livewire.
// For this reason, I'm using an "on-demand" proxy to fake a "$refs" object.
return new Proxy({}, {
get(object, property) {
var ref

// We can't just query the DOM because it's hard to filter out refs in
// nested components.
walkSkippingNestedComponents(self.el, el => {
if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
ref = el
}
})

return ref
}
})
}
}
6 changes: 3 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function walkSkippingNestedComponents(el, callback) {
while (node) {
if (node.hasAttribute('x-data')) return

walk(node, callback)
walkSkippingNestedComponents(node, callback)
node = node.nextElementSibling
}
}
Expand Down Expand Up @@ -61,7 +61,7 @@ export function saferEvalNoReturn(expression, dataContext, additionalHelperVaria
}

export function isXAttr(attr) {
const xAttrRE = /x-(on|bind|data|text|model|cloak)/
const xAttrRE = /x-(on|bind|data|text|model|cloak|ref)/

return xAttrRE.test(attr.name)
}
Expand All @@ -70,7 +70,7 @@ export function getXAttrs(el, type) {
return Array.from(el.attributes)
.filter(isXAttr)
.map(attr => {
const typeMatch = attr.name.match(/x-(on|bind|data|text|model|cloak)/)
const typeMatch = attr.name.match(/x-(on|bind|data|text|model|cloak|ref)/)
const valueMatch = attr.name.match(/:([a-zA-Z\-]+)/)
const modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []

Expand Down
24 changes: 24 additions & 0 deletions test/ref.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import projectX from 'project-x'
import { wait } from 'dom-testing-library'

global.MutationObserver = class {
observe() {}
}

test('can reference elements from event listeners', async () => {
document.body.innerHTML = `
<div x-data="{}">
<span x-ref="bob"></span>
<button x-on:click="$refs['bob'].innerText = 'lob'"></button>
</div>
`

projectX.start()

expect(document.querySelector('span').innerText).toEqual(undefined)

document.querySelector('button').click()

await wait(() => { expect(document.querySelector('span').innerText).toEqual('lob') })
})

0 comments on commit 47fddce

Please sign in to comment.