Skip to content

Commit 403d1f9

Browse files
committed
Handle nested properties and methods for $returnValue.
1 parent a7db9f6 commit 403d1f9

File tree

4 files changed

+154
-46
lines changed

4 files changed

+154
-46
lines changed

django_unicorn/static/js/eventListeners.js

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,50 @@ function handleLoading(component, targetElement) {
4646
});
4747
}
4848

49+
/**
50+
* Parse arguments and deal with nested data.
51+
*
52+
* // <button u:click="test($returnValue.hello.trim())">Test</button>
53+
* let data = {hello: " world "};
54+
* let output = parseEventArg(data, "$returnValue.hello.trim()");
55+
* // output is 'world'
56+
*
57+
* @param {Object} data The data that should be parsed.
58+
* @param {String} arg The argument to the function.
59+
* @param {String} specialArgName The special argument (starts with a $).
60+
*/
61+
function parseEventArg(data, arg, specialArgName) {
62+
// Remove any extra whitespace, everything before and including "$event", and the ending paren
63+
arg = arg
64+
.trim()
65+
.slice(arg.indexOf(specialArgName) + specialArgName.length)
66+
.trim();
67+
68+
arg.split(".").forEach((piece) => {
69+
piece = piece.trim();
70+
71+
if (piece) {
72+
// TODO: Handle method calls with args
73+
if (piece.endsWith("()")) {
74+
// method call
75+
const methodName = piece.slice(0, piece.length - 2);
76+
data = data[methodName]();
77+
} else if (hasValue(data[piece])) {
78+
data = data[piece];
79+
} else {
80+
throw Error(`'${piece}' could not be retrieved`);
81+
}
82+
}
83+
});
84+
85+
if (typeof data === "string") {
86+
// Wrap strings in quotes
87+
data = `"${data}"`;
88+
}
89+
90+
return data;
91+
}
92+
4993
/**
5094
* Adds an action event listener to the document for each type of event (e.g. click, keyup, etc).
5195
* Added at the document level because validation errors would sometimes remove the
@@ -128,48 +172,31 @@ export function addActionEventListener(component, eventType) {
128172
// Handle special arguments (e.g. $event)
129173
args(action.name).forEach((eventArg) => {
130174
if (eventArg.startsWith("$event")) {
131-
// Remove any extra whitespace, everything before and including "$event", and the ending paren
132-
eventArg = eventArg
133-
.trim()
134-
.slice(eventArg.indexOf("$event") + 6)
135-
.trim();
136-
137-
const originalSpecialVariable = `$event${eventArg}`;
138-
let data = event;
139-
let invalidPiece = false;
140-
141-
eventArg.split(".").forEach((piece) => {
142-
piece = piece.trim();
143-
144-
if (piece) {
145-
// TODO: Handle method calls with args
146-
if (piece.endsWith("()")) {
147-
// method call
148-
const methodName = piece.slice(0, piece.length - 2);
149-
data = data[methodName]();
150-
} else if (hasValue(data[piece])) {
151-
data = data[piece];
152-
} else {
153-
invalidPiece = true;
154-
}
155-
}
156-
});
157-
158-
if (invalidPiece) {
159-
console.error(
160-
`'${originalSpecialVariable}' could not be retrieved`
161-
);
162-
action.name = action.name.replace(originalSpecialVariable, "");
163-
} else if (data) {
164-
if (typeof data === "string") {
165-
// Wrap strings in quotes
166-
data = `"${data}"`;
175+
try {
176+
const data = parseEventArg(event, eventArg, "$event");
177+
action.name = action.name.replace(eventArg, data);
178+
} catch (err) {
179+
// console.error(err);
180+
action.name = action.name.replace(eventArg, "");
181+
}
182+
} else if (eventArg.startsWith("$returnValue")) {
183+
if (
184+
hasValue(component.return) &&
185+
hasValue(component.return.value)
186+
) {
187+
try {
188+
const data = parseEventArg(
189+
component.return.value,
190+
eventArg,
191+
"$returnValue"
192+
);
193+
action.name = action.name.replace(eventArg, data);
194+
} catch (err) {
195+
// console.error(err);
196+
action.name = action.name.replace(eventArg, "");
167197
}
168-
169-
action.name = action.name.replace(
170-
originalSpecialVariable,
171-
data
172-
);
198+
} else {
199+
action.name = action.name.replace(eventArg, "");
173200
}
174201
} else if (eventArg === "$model") {
175202
const db = {};

example/unicorn/components/text_inputs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from django.utils.timezone import now
2+
13
from django_unicorn.components import UnicornView
24

35

@@ -9,3 +11,5 @@ def set_name(self, name=None):
911
self.name = name
1012
else:
1113
self.name = "Universe"
14+
15+
return f"{self.name} - {now().second}"

example/unicorn/templates/unicorn/text-inputs.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,17 @@
3333
<label>Multiple actions</label>
3434
<input unicorn:model="name" type="text" unicorn:keyup.enter="name='enter'" unicorn:keydown.escape="name=''"></input>
3535

36+
<br />
37+
Hello {{ name|title }} 👋
38+
3639
<br />
3740
<button unicorn:click="set_name">set_name</button>
3841
<button unicorn:click="set_name()">set_name()</button>
3942
<button unicorn:click="set_name('')">set_name('')</button>
4043
<button unicorn:click="set_name('blob 2')">set_name('blob 2')</button>
4144
<button unicorn:click="name='human'">name='human'</button>
4245

43-
<br />
44-
Hello {{ name|title }} 👋
45-
46-
<br />
46+
<br /><br />
4747
<button unicorn:click='set_name($event.target.value.trim())' value=' button value '>set_name($event.target.value.trim())</button>
48+
<button unicorn:click="set_name($returnValue.trim())">set_name($returnValue)</button>
4849
</div>

tests/js/element/action.test.js

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,82 @@ test("click on internal element", (t) => {
6666
t.is(component.actionQueue.length, 1);
6767
});
6868

69+
test("$returnValue", (t) => {
70+
const html = `
71+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
72+
<input unicorn:model='name'></input>
73+
<button unicorn:click='test($returnValue)'></button>
74+
</div>`;
75+
const component = getComponent(html);
76+
component.return = { value: "123" };
77+
78+
t.is(component.attachedEventTypes.length, 1);
79+
t.is(component.actionEvents.click.length, 1);
80+
81+
component.actionEvents.click[0].element.el.click();
82+
83+
t.is(component.actionQueue.length, 1);
84+
const action = component.actionQueue[0];
85+
t.is(action.payload.name, 'test("123")');
86+
});
87+
88+
test("$returnValue invalid property", (t) => {
89+
const html = `
90+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
91+
<input unicorn:model='name'></input>
92+
<button unicorn:click='test($returnValue.blob)'></button>
93+
</div>`;
94+
const component = getComponent(html);
95+
component.return = { value: "123" };
96+
97+
t.is(component.attachedEventTypes.length, 1);
98+
t.is(component.actionEvents.click.length, 1);
99+
100+
component.actionEvents.click[0].element.el.click();
101+
102+
t.is(component.actionQueue.length, 1);
103+
const action = component.actionQueue[0];
104+
t.is(action.payload.name, "test()");
105+
});
106+
107+
test("$returnValue nested property", (t) => {
108+
const html = `
109+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
110+
<input unicorn:model='name'></input>
111+
<button unicorn:click='test($returnValue.hello)'></button>
112+
</div>`;
113+
const component = getComponent(html);
114+
component.return = { value: { hello: "world" } };
115+
116+
t.is(component.attachedEventTypes.length, 1);
117+
t.is(component.actionEvents.click.length, 1);
118+
119+
component.actionEvents.click[0].element.el.click();
120+
121+
t.is(component.actionQueue.length, 1);
122+
const action = component.actionQueue[0];
123+
t.is(action.payload.name, 'test("world")');
124+
});
125+
126+
test("$returnValue with method", (t) => {
127+
const html = `
128+
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
129+
<input unicorn:model='name'></input>
130+
<button unicorn:click='test($returnValue.trim())'></button>
131+
</div>`;
132+
const component = getComponent(html);
133+
component.return = { value: " world " };
134+
135+
t.is(component.attachedEventTypes.length, 1);
136+
t.is(component.actionEvents.click.length, 1);
137+
138+
component.actionEvents.click[0].element.el.click();
139+
140+
t.is(component.actionQueue.length, 1);
141+
const action = component.actionQueue[0];
142+
t.is(action.payload.name, 'test("world")');
143+
});
144+
69145
test("$event action variable invalid property", (t) => {
70146
const html = `
71147
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
@@ -84,7 +160,7 @@ test("$event action variable invalid property", (t) => {
84160
t.is(action.payload.name, 'test("1")');
85161
});
86162

87-
test("$event action variable", (t) => {
163+
test("$event invalid variable", (t) => {
88164
const html = `
89165
<div unicorn:id="5jypjiyb" unicorn:name="text-inputs" unicorn:checksum="GXzew3Km">
90166
<input unicorn:model='name'></input>

0 commit comments

Comments
 (0)