Skip to content

Correctly Handle Target Event Data #550

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

Merged
merged 4 commits into from
Dec 16, 2021
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async def handle_submit(event):
html.select(
{
"value": recipient,
"onChange": lambda event: set_recipient(event["value"]),
"onChange": lambda event: set_recipient(event["target"]["value"]),
},
html.option({"value": "Alice"}, "Alice"),
html.option({"value": "Bob"}, "Bob"),
Expand All @@ -33,7 +33,7 @@ async def handle_submit(event):
"type": "text",
"placeholder": "Your message...",
"value": message,
"onChange": lambda event: set_message(event["value"]),
"onChange": lambda event: set_message(event["target"]["value"]),
}
),
html.button({"type": "submit"}, "Send"),
Expand Down
2 changes: 1 addition & 1 deletion docs/source/adding-interactivity/_examples/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def handle_submit(event):
{
"placeholder": "Your message here...",
"value": message,
"onChange": lambda event: set_message(event["value"]),
"onChange": lambda event: set_message(event["target"]["value"]),
}
),
html.button({"type": "submit"}, "Send"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def ExpandableNumberInputs(values, set_values):
for i in range(len(values)):

def set_value_at_index(event, index=i):
new_value = float(event["value"] or 0)
new_value = float(event["target"]["value"] or 0)
set_values(values[:index] + [new_value] + values[index + 1 :])

inputs.append(poly_coef_input(i + 1, set_value_at_index))
Expand Down
2 changes: 1 addition & 1 deletion docs/source/reference-material/_examples/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def Todo():

async def add_new_task(event):
if event["key"] == "Enter":
set_items(items + [event["value"]])
set_items(items + [event["target"]["value"]])

tasks = []

Expand Down
2 changes: 2 additions & 0 deletions src/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/client/packages/idom-client-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"description": "A client for IDOM implemented in React",
"devDependencies": {
"jsdom": "16.3.0",
"lodash": "^4.17.21",
"prettier": "^2.5.1",
"uvu": "^0.5.1"
},
Expand All @@ -25,8 +26,8 @@
"url": "https://github.com/idom-team/idom"
},
"scripts": {
"format": "prettier --write ./src",
"check-format": "prettier --check ./src",
"format": "prettier --write ./src ./tests",
"check-format": "prettier --check ./src ./tests",
"test": "uvu tests"
},
"type": "module",
Expand Down
58 changes: 38 additions & 20 deletions src/client/packages/idom-client-react/src/event-to-object.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,40 @@ export function serializeEvent(event) {
Object.assign(data, eventTransforms[event.type](event));
}

const target = event.target;
if (target.tagName in targetTransforms) {
targetTransforms[target.tagName].forEach((trans) =>
Object.assign(data, trans(target))
);
}
data.target = serializeDomElement(event.target);
data.currentTarget =
event.target === event.currentTarget
? data.target
: serializeDomElement(event.currentTarget);
data.relatedTarget = serializeDomElement(event.relatedTarget);

return data;
}

const targetTransformCategories = {
hasValue: (target) => ({
value: target.value,
function serializeDomElement(element) {
let elementData = null;
if (element) {
elementData = defaultElementTransform(element);
if (element.tagName in elementTransforms) {
elementTransforms[element.tagName].forEach((trans) =>
Object.assign(elementData, trans(element))
);
}
}
return elementData;
}

const elementTransformCategories = {
hasValue: (element) => ({
value: element.value,
}),
hasCurrentTime: (target) => ({
currentTime: target.currentTime,
hasCurrentTime: (element) => ({
currentTime: element.currentTime,
}),
hasFiles: (target) => {
if (target?.type === "file") {
hasFiles: (element) => {
if (element?.type === "file") {
return {
files: Array.from(target.files).map((file) => ({
files: Array.from(element.files).map((file) => ({
lastModified: file.lastModified,
name: file.name,
size: file.size,
Expand All @@ -38,7 +51,11 @@ const targetTransformCategories = {
},
};

const targetTagCategories = {
function defaultElementTransform(element) {
return { boundingClientRect: element.getBoundingClientRect() };
}

const elementTagCategories = {
hasValue: [
"BUTTON",
"INPUT",
Expand All @@ -54,12 +71,13 @@ const targetTagCategories = {
hasFiles: ["INPUT"],
};

const targetTransforms = {};
const elementTransforms = {};

Object.keys(targetTagCategories).forEach((category) => {
targetTagCategories[category].forEach((type) => {
const transforms = targetTransforms[type] || (targetTransforms[type] = []);
transforms.push(targetTransformCategories[category]);
Object.keys(elementTagCategories).forEach((category) => {
elementTagCategories[category].forEach((type) => {
const transforms =
elementTransforms[type] || (elementTransforms[type] = []);
transforms.push(elementTransformCategories[category]);
});
});

Expand Down
71 changes: 57 additions & 14 deletions src/client/packages/idom-client-react/tests/event-to-object.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import { test } from "uvu";
import lodash from "lodash";
import * as assert from "uvu/assert";
import { serializeEvent } from "../src/event-to-object.js";
import "./tooling/setup.js";

function assertEqualSerializedEventData(eventData, expectedSerializedData) {
const mockBoundingRect = {
left: 0,
top: 0,
right: 0,
bottom: 0,
x: 0,
y: 0,
width: 0,
};

const mockElement = {
tagName: null,
getBoundingClientRect: () => mockBoundingRect,
};

const commonEventData = {
target: mockElement,
currentTarget: mockElement,
relatedTarget: mockElement,
};

const commonSerializedEventData = {
target: { boundingClientRect: mockBoundingRect },
currentTarget: { boundingClientRect: mockBoundingRect },
relatedTarget: { boundingClientRect: mockBoundingRect },
};

assert.equal(
serializeEvent(lodash.merge({}, commonEventData, eventData)),
lodash.merge({}, commonSerializedEventData, expectedSerializedData)
);
}

const allTargetData = {
files: [
{
Expand All @@ -21,33 +56,38 @@ const allTargetData = {
{
case: "adds 'files' and 'value' attributes for INPUT if type=file",
tagName: "INPUT",
otherAttrs: { type: "file" },
addTargetAttrs: { type: "file" },
output: {
files: allTargetData.files,
value: allTargetData.value,
target: {
files: allTargetData.files,
value: allTargetData.value,
},
},
},
...["BUTTON", "INPUT", "OPTION", "LI", "METER", "PROGRESS", "PARAM"].map(
(tagName) => ({
case: `adds 'value' attribute for ${tagName} element`,
tagName,
output: { value: allTargetData.value },
output: { target: { value: allTargetData.value } },
})
),
...["AUDIO", "VIDEO"].map((tagName) => ({
case: `adds 'currentTime' attribute for ${tagName} element`,
tagName,
output: { currentTime: allTargetData.currentTime },
output: { target: { currentTime: allTargetData.currentTime } },
})),
].forEach((expectation) => {
test(`serializeEvent() ${expectation.case}`, () => {
const eventData = {
target: { ...allTargetData, tagName: expectation.tagName },
target: {
...allTargetData,
tagName: expectation.tagName,
},
};
if (expectation.otherAttrs) {
Object.assign(eventData.target, expectation.otherAttrs);
if (expectation.addTargetAttrs) {
Object.assign(eventData.target, expectation.addTargetAttrs);
}
assert.equal(serializeEvent(eventData), expectation.output);
assertEqualSerializedEventData(eventData, expectation.output);
});
});

Expand Down Expand Up @@ -247,8 +287,8 @@ const allEventData = {
},
].forEach((expectation) => {
test(`serializeEvent() adds ${expectation.case} attributes`, () => {
assert.equal(
serializeEvent({ ...allEventData, type: expectation.eventType }),
assertEqualSerializedEventData(
{ ...allEventData, type: expectation.eventType },
expectation.output
);
});
Expand All @@ -267,9 +307,12 @@ test("serializeEvent() adds text of current selection", () => {
const start = document.getElementById("start");
const end = document.getElementById("end");
window.getSelection().setBaseAndExtent(start, 0, end, 0);
assert.equal(serializeEvent({ ...allEventData, type: "select" }), {
selectedText: "START\nMIDDLE\n",
});
assertEqualSerializedEventData(
{ ...allEventData, type: "select" },
{
selectedText: "START\nMIDDLE\n",
}
);
});

test.run();
38 changes: 38 additions & 0 deletions src/idom/core/_event_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Any, Dict, Sequence
from warnings import warn


def _wrap_in_warning_event_proxies(values: Sequence[Any]) -> Sequence[Any]:
return [_EventProxy(x) if isinstance(x, dict) else x for x in values]


class _EventProxy(Dict[Any, Any]):
def __getitem__(self, key: Any) -> Any: # pragma: no cover
try:
return super().__getitem__(key)
except KeyError:
target = self.get("target")
if isinstance(target, dict) and key in target:
warn(
f"The event key event[{key!r}] has been moved event['target'][{key!r}",
DeprecationWarning,
stacklevel=2,
)
return target[key]
else:
raise

def get(self, key: Any, default: Any = None) -> Any: # pragma: no cover
try:
return super().__getitem__(key)
except KeyError:
target = self.get("target")
if isinstance(target, dict) and key in target:
warn(
f"The event key event[{key!r}] has been moved event['target'][{key!r}",
DeprecationWarning,
stacklevel=2,
)
return target[key]
else:
return default
10 changes: 5 additions & 5 deletions src/idom/core/events.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import asyncio
from typing import Any, Callable, List, Optional, Sequence, overload
from typing import Any, Callable, Optional, Sequence, overload

from anyio import create_task_group
from typing_extensions import Literal
Expand Down Expand Up @@ -145,18 +145,18 @@ def to_event_handler_function(
if positional_args:
if asyncio.iscoroutinefunction(function):

async def wrapper(data: List[Any]) -> None:
async def wrapper(data: Sequence[Any]) -> None:
await function(*data)

else:

async def wrapper(data: List[Any]) -> None:
async def wrapper(data: Sequence[Any]) -> None:
function(*data)

return wrapper
elif not asyncio.iscoroutinefunction(function):

async def wrapper(data: List[Any]) -> None:
async def wrapper(data: Sequence[Any]) -> None:
function(data)

return wrapper
Expand Down Expand Up @@ -212,7 +212,7 @@ def merge_event_handler_funcs(
elif len(functions) == 1:
return functions[0]

async def await_all_event_handlers(data: List[Any]) -> None:
async def await_all_event_handlers(data: Sequence[Any]) -> None:
async with create_task_group() as group:
for func in functions:
group.start_soon(func, data)
Expand Down
3 changes: 2 additions & 1 deletion src/idom/core/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from idom.utils import Ref

from ._event_proxy import _wrap_in_warning_event_proxies
from .hooks import LifeCycleHook
from .proto import ComponentType, EventHandlerDict, VdomJson
from .vdom import validate_vdom_json
Expand Down Expand Up @@ -119,7 +120,7 @@ async def deliver(self, event: LayoutEvent) -> None:

if handler is not None:
try:
await handler.function(event.data)
await handler.function(_wrap_in_warning_event_proxies(event.data))
except Exception:
logger.exception(f"Failed to execute event handler {handler}")
else:
Expand Down
2 changes: 1 addition & 1 deletion src/idom/core/proto.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class _JsonImportSource(TypedDict):
class EventHandlerFunc(Protocol):
"""A coroutine which can handle event data"""

async def __call__(self, data: List[Any]) -> None:
async def __call__(self, data: Sequence[Any]) -> None:
...


Expand Down
2 changes: 1 addition & 1 deletion src/idom/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def Input(
value, set_value = idom.hooks.use_state(value)

def on_change(event: Dict[str, Any]) -> None:
value = event["value"]
value = event["target"]["value"]
set_value(value)
if not value and ignore_empty:
return
Expand Down
Loading