From a1fa1a5873bc382547bcd34334cdbd6f29e35844 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:35:22 +0000 Subject: [PATCH] Render the tree on the client (#126) * Add serialization and deserialization on the client * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove extra comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/component-philospy.md | 9 ++ starfyre/__init__.py | 1 - starfyre/component.py | 21 +++- starfyre/dist_builder.py | 29 ++--- starfyre/dom_methods.py | 86 ++++++++----- starfyre/js/dom_helpers.py | 121 ++++++++++++++++++ starfyre/js/dom_methods.py | 74 ----------- starfyre/js/store.js | 55 -------- starfyre/js/store.py | 159 +++++++++--------------- starfyre/parser.py | 40 +++--- test_application/components/parent.fyre | 2 +- test_application/pages/__init__.fyre | 7 +- 12 files changed, 302 insertions(+), 302 deletions(-) create mode 100644 docs/component-philospy.md create mode 100644 starfyre/js/dom_helpers.py delete mode 100644 starfyre/js/dom_methods.py delete mode 100644 starfyre/js/store.js diff --git a/docs/component-philospy.md b/docs/component-philospy.md new file mode 100644 index 0000000..8bb408e --- /dev/null +++ b/docs/component-philospy.md @@ -0,0 +1,9 @@ +## Component Philosophy + +Every component in Starfyre will be a stateless component. This means that the component will not have any state of its own. + +Should there be a need for a stateful component, the state will be treated like a unique entity. + +Just by including a state in the component, the component will be subscribed to the state. This means that the component will be re-rendered every time the state changes. + +The state doesn't have to be a global state. It can be a local state too. We don't need to do prop drilling to pass the state to the component. The component will be subscribed to the state, and it will be re-rendered every time the state changes. diff --git a/starfyre/__init__.py b/starfyre/__init__.py index 1f297bf..d532198 100644 --- a/starfyre/__init__.py +++ b/starfyre/__init__.py @@ -31,7 +31,6 @@ def create_component( props={}, children=[], event_listeners={}, - state={}, uuid="store", js=js, original_name="div", diff --git a/starfyre/component.py b/starfyre/component.py index 7ea912b..3311971 100644 --- a/starfyre/component.py +++ b/starfyre/component.py @@ -8,14 +8,16 @@ class Component: props: dict children: list event_listeners: dict - state: dict uuid: Any signal: str = "" original_data: str = "" data: str = "" parentComponent: Optional[Any] = None + # html,css, and js are debug properties. Not needed for rendering html: str = "" + # this should not be a part of the rendering css: str = "" + # this should not be a part of the rendering js: str = "" client_side_python: str = "" original_name: str = "" @@ -31,3 +33,20 @@ def is_slot_component(self): def __repr__(self): return f"<{self.tag}> {self.data} {self.children} " + + def to_json(self): + return { + "tag": self.tag, + "props": self.props, + "children": self.children, + "event_listeners": self.event_listeners, + "uuid": self.uuid, + "signal": self.signal, + "original_data": self.original_data, + "data": self.data, + "parentComponent": self.parentComponent, + "html": self.html, + "original_name": self.original_name, + } + + # not including the css, js and client_side_python as they are not needed for re rendering diff --git a/starfyre/dist_builder.py b/starfyre/dist_builder.py index 8a3bc7a..cbabdc7 100644 --- a/starfyre/dist_builder.py +++ b/starfyre/dist_builder.py @@ -12,28 +12,16 @@ """ -def write_js_file(path: Path): - dist_path = Path(path) / "dist" - print("This is the dist path", dist_path) - dist_path.mkdir(exist_ok=True) - - with pkg_resources.path("starfyre.js", "store.js") as js_store: - store_path = dist_path / "store.js" - print("This is the store path", store_path) - print("This is the js store path", js_store) - shutil.copy(str(js_store), str(store_path)) - - def write_python_client_file(path: Path): dist_path = Path(path) / "dist" dist_path.mkdir(exist_ok=True) - with pkg_resources.path( - "starfyre.js", "dom_methods.py" - ) as dom_methods, pkg_resources.path("starfyre.js", "store.py") as store_py: - dom_methods_path = dist_path / "dom_methods.py" - shutil.copy(str(dom_methods), str(dom_methods_path)) + with pkg_resources.path("starfyre.js", "store.py") as store_py, pkg_resources.path( + "starfyre.js", "dom_helpers.py" + ) as dom_helpers: store_path = dist_path / "store.py" shutil.copy(str(store_py), str(store_path)) + dom_helpers_path = dist_path / "dom_helpers.py" + shutil.copy(str(dom_helpers), str(dom_helpers_path)) def generate_html_pages(file_routes, project_dir: Path): @@ -98,8 +86,11 @@ def generate_html_pages(file_routes, project_dir: Path): html_file.write( "" ) + html_file.write( + "" + ) + html_file.write("") html_file.write("") - html_file.write("") html_file.write(rendered_page) # Change back to the original directory @@ -167,8 +158,6 @@ def create_dist(file_routes, project_dir_path): """ print("This is the project dir path", project_dir_path) print("These are the file routes", file_routes) - write_js_file(project_dir_path) - print("JS file written") write_python_client_file(project_dir_path) print("Python files written") diff --git a/starfyre/dom_methods.py b/starfyre/dom_methods.py index 27c514e..fc1dc92 100644 --- a/starfyre/dom_methods.py +++ b/starfyre/dom_methods.py @@ -1,6 +1,5 @@ -import re -from functools import partial -from uuid import uuid4 +import json +from uuid import UUID, uuid4 from .component import Component @@ -34,7 +33,30 @@ def is_attribute(name): return not is_listener(name) and name != "children" -def render_helper(component: Component) -> tuple[str, str, str, str]: +def assign_initial_signal_population(component: Component): + return f""" +component = js.document.querySelector("[data-pyxide-id='{component.uuid}']"); +if (component): + component.innerText = {component.data} + """ + + +def hydration_helper(component: Component) -> tuple[str, str, str, str]: + """ + Args: + component (Component): The root component + + We are just hyrdating the html here, and then the client side python should populate the html + with the new values and attach the event listeners, signal, etc. + + TODO: + I want to find a way for signals to be populated with a default value on the server side or + something similar for more complex components. + + The inital value should be filled in the html, which we are already doing and then everything + else should be attached on the client side. + """ + parentElement = component.parentComponent html = "\n" css = "" @@ -47,7 +69,6 @@ def render_helper(component: Component) -> tuple[str, str, str, str]: props={"id": "root"}, children=[], event_listeners={}, - state={}, uuid=uuid4(), original_name="div", ) @@ -55,36 +76,21 @@ def render_helper(component: Component) -> tuple[str, str, str, str]: tag = component.tag props = component.props - state = component.state data = component.data event_listeners = component.event_listeners # Create DOM element if component.is_text_component: - # find all the names in "{}" and print them - matches = re.findall(r"{(.*?)}", data) - for match in matches: - if match in state: - function = state[match] - function = partial(function, component) - data = component.data.replace(f"{{{ match }}}", str(function())) - else: - print( - "No match found for", match, component, "This is the state", state - ) - component.parentComponent.uuid = component.uuid html += f"{data}\n" component.html = html + # matches = re.findall(r"{(.*?)}", data) + # print("This is the matches", matches) + # we need to do a better way of managing the signals if component.signal: - client_side_python += f""" -component = js.document.querySelector("[data-pyxide-id='{component.uuid}']"); -js.addDomIdToMap('{component.uuid}', "{component.signal}"); -if (component): - component.innerText = {component.signal} - """ - + # TODO: this part should be moved to the client + client_side_python += assign_initial_signal_population(component) return html, css, js, client_side_python if component.css: @@ -121,7 +127,9 @@ def render_helper(component: Component) -> tuple[str, str, str, str]: for childElement in children: childElement.parentElement = component - new_html, new_css, new_js, new_client_side_python = render_helper(childElement) + new_html, new_css, new_js, new_client_side_python = hydration_helper( + childElement + ) html += new_html css += new_css js += new_js @@ -134,13 +142,27 @@ def render_helper(component: Component) -> tuple[str, str, str, str]: return html, css, js, client_side_python +class ComponentEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Component): + return obj.to_json() + + if isinstance(obj, UUID): + return str(obj) + # Let the base class default method raise the TypeError + return json.JSONEncoder.default(self, obj) + + def hydrate(component: Component) -> str: - html, css, js, client_side_python = render_helper(component) + html, css, js, client_side_python = hydration_helper(component) + tree = json.dumps(component, cls=ComponentEncoder) final_html = f""" - - - -
{html}
-""" + + + + + +
{html}
+ """ return final_html diff --git a/starfyre/js/dom_helpers.py b/starfyre/js/dom_helpers.py new file mode 100644 index 0000000..bce83dc --- /dev/null +++ b/starfyre/js/dom_helpers.py @@ -0,0 +1,121 @@ +import ujson as json +import js + + +def is_listener(name): + return name.startswith("on") + + +def is_attribute(name): + return not is_listener(name) and name != "children" + + +class Component: + def __init__( + self, + tag: str, + props: dict, + children: list, + event_listeners: dict, + uuid: str, + signal: str = "", + original_data: str = "", + data: str = "", + parentComponent=None, + html: str = "", + css: str = "", + js: str = "", + client_side_python: str = "", + original_name: str = "", + ): + self.tag = tag + self.props = props + self.children = children + self.event_listeners = event_listeners + self.uuid = uuid + self.signal = signal + self.original_data = data + self.data = data + self.parentComponent = parentComponent + self.html = html + self.css = css + self.js = js + self.client_side_python = client_side_python + self.original_name = original_name + + @property + def is_text_component(self): + return self.tag == "TEXT_NODE" + + @property + def is_slot_component(self): + return self.tag == "slot" + + def __repr__(self): + return f"<{self.tag}> {self.data} {self.children} " + + def re_render_helper(self, component): + # this will rebuild the tree + ... + + if self == component: + print("This is the true component", component) + + for child in component.children: + if isinstance(child, Component): + self.re_render_helper(child) + + def re_render(self): + print("This is the re render function") + return self.re_render_helper(self) + + +def parse_component_data(data): + if isinstance(data, dict): + # Check if this dictionary represents a Component + if "tag" in data and "props" in data: + # Parse children if present + children = data.get("children", []) + parsed_children = [parse_component_data(child) for child in children] + + # Handle special cases like parentComponent + if "parentComponent" in data and data["parentComponent"] is not None: + data["parentComponent"] = parse_component_data(data["parentComponent"]) + + # Create a Component instance, passing the parsed children + data["children"] = parsed_children + component = Component(**data) + dom_id = component.uuid + + # need to find a way to add this component to a global dom store + # and make it accessible to the render function + print("This is the dom id", clientDomIdMap) + clientDomIdMap[dom_id] = component + return component + else: + # Handle other dict structures (e.g., props) + return {k: parse_component_data(v) for k, v in data.items()} + elif isinstance(data, list): + # Handle lists (e.g., a list of children) + return [parse_component_data(item) for item in data] + else: + # Return the item as is for basic types (int, str, etc.) + return data + + +def rebuild_tree(): + print("This is the rebuild tree function") + # this is present globally + # TODO: need to work on this + # js.window maybe + STARFYRE_ROOT_NODE = getattr(js.window, "STARFYRE_ROOT_NODE") + tree_node = STARFYRE_ROOT_NODE + # print("This is the tree node", tree_node) + print("This is the tree node", type(tree_node)) + json_data = json.loads(tree_node) + tree = parse_component_data(json_data) + print("Successfully loaded json data", json_data) + print(tree) + + +rebuild_tree() diff --git a/starfyre/js/dom_methods.py b/starfyre/js/dom_methods.py deleted file mode 100644 index 50f2b95..0000000 --- a/starfyre/js/dom_methods.py +++ /dev/null @@ -1,74 +0,0 @@ -def assign_event_listeners(component, event_listeners): - for event_listener_name, event_listener in event_listeners.items(): - event_type = event_listener_name.lower()[2:] - print( - "Assigning event listeners to the component", - component, - event_type, - event_listener, - ) - # component.dom.addEventListener(event_type, create_proxy(event_listener)) - - -# def render(component: Component): -# parentElement = component.parentComponent -# if parentElement is None: -# parentElement = js.document.createElement("div") -# parentElement.id = "root" -# js.document.body.appendChild(parentElement) - -# component.parentComponent = parentElement -# we will add rust later -# dom_node = DomNode(element.tag, element.props, element.children, element.event_listeners, element.state or {}) -# dom_node.set_parent_element(parentComponent) -# print(dom_node) - -# tag = component.tag -# props = component.props -# state = component.state -# data = component.data -# print(data) - -# Create DOM element -# if component.is_text_component: -# find all the names in "{}" and print them -# matches = re.findall(r"{(.*?)}", data) -# for match in matches: -# if match in state: -# function = state[match] -# function = partial(function, component) -# data = component.data.replace(f"{{{ match }}}", str(function())) -# dom = js.document.createTextNode(data) -# print("Text Node", component) -# else: -# dom = js.document.createElement(tag) - -# component.dom = dom - -# Add event listeners -# def isListener(name): -# return name.startswith("on") -# def isAttribute(name): -# return not isListener(name) and name != "children" - -# assign_event_listeners(component, component.event_listeners) -# set attributes - -# for name in props: -# if isAttribute(name): -# dom.setAttribute(name, props[name]) - -# Render children -# children = component.children -# childElements.forEach(childElement => render(childElement, dom)); -# for childElement in children: -# childElement.parentComponent = dom -# render(childElement) - -# // Append to parent -# need to apply batch updates here -# also create a tree of dom nodes in the first place -# if parentElement.contains(dom): -# parentElement.replaceChild(dom, parentElement.childNodes[0]) -# else: -# parentElement.appendChild(dom) diff --git a/starfyre/js/store.js b/starfyre/js/store.js deleted file mode 100644 index 6b046a5..0000000 --- a/starfyre/js/store.js +++ /dev/null @@ -1,55 +0,0 @@ -observers = {}; // uuid , list[observers] - -const domIdMap = {}; - -function addDomIdToMap(domId, pyxide) { - domIdMap[domId] = pyxide; -} - -function getPyxideFromDomId(domId) { - return domIdMap[domId]; -} - -function render(domId) { - const element = document.querySelector(`[data-pyxide-id="${domId}"]`); - const pyxide = getPyxideFromDomId(domId); - element.innerHTML = `${eval(pyxide)}`; -} - -// function create_signal(initial_state) { -// let id = Math.random() * 1000000000000000; // simulating uuid -// let state = initial_state; - -// function use_signal(domId) { -// if (!domId) { -// // when the domId is not provided, it means that the signal is used in a function component -// return store[id] || initial_state; -// } - -// if (observers[id]) { -// observers[id].push(domId); -// } else { -// observers[id] = [domId]; -// } - -// return state; -// } - -// function set_signal(newState) { -// state = newState; - -// if (!observers[id]) { -// return; -// } - -// observers[id].forEach((element) => { -// render(element); -// }); -// } - -// function get_signal() { -// return state; -// } - -// return [use_signal, set_signal, get_signal]; -// } diff --git a/starfyre/js/store.py b/starfyre/js/store.py index 7aaf7d3..94a408f 100644 --- a/starfyre/js/store.py +++ b/starfyre/js/store.py @@ -4,133 +4,96 @@ import js -# from .dom_methods import render -store = {} -observers = {} - -reverse_store = {} # maps dom id: uuid -# dict[uuid.UUID, list[Component]] = -# - - -def render(component, parentElement=None): # this is domElement - # the best fix will be to serialize the dom tree and then de serialize it - # on the client - # and then find from a dict to re render +store = GLOBAL_STORE +observers = GLOBAL_OBSERVERS + +reverse_store = GLOBAL_OBSERVERS # maps dom id: uuid +clientDomIdMap = GLOBAL_CLIENT_DOM_ID_MAP + + +def evaluate_data(component, dom_id): + # we will need to extend this + data = component.data + new_data = data + print("Original data", data) + if "signal" in data: + store_id = reverse_store.get(dom_id) + signal_value = store.get(store_id) + print("This is the component signal", component.signal) + temp_data = data.replace(component.signal, str(signal_value)) + print("This is the temp data", temp_data) + new_data = eval(f"{temp_data.strip().strip(';')}") + print("This is the new data", new_data) + + return new_data + + +def hydrate(component): + # this will take the component and then find the dom element + # and then convert the tree to the html + # component is then tree + # and pareneComponent is the parent node + # we need to remove the html of the parent component + # and then add the new html based on the component tree # + for child in component.children: + id = child.uuid + domElement = js.document.querySelector(f"[data-pyxide-id='{id}']") + if domElement is None: + continue - # but for now, we will just find the id of the component by - # document.getElementById - # and then get the current state - # put it as a inner text and then re render the children - # - - if parentElement is None: - parentElement = js.document.querySelector("[data-pyxide-id=root]") - if parentElement is None: - parentElement = js.document.createElement("div") - parentElement.id = "root" - js.document.body.appendChild(parentElement) - - if component: - # find all the names in "{}" and print them - # need to sort this again - # matches = re.findall(r"{(.*?)}", data) - # for match in matches: - # if match in state: - # function = state[match] - # function = partial(function, component) - # data = component.data.replace(f"{{{ match }}}", str(function())) - # dom = js.document.createTextNode(data) - - dom_id = component.id - - parentElement.appendChild(component) - # print("Text Node", component) - js.console.log(component, component.children) - if component.children: - for child in component.children: - render(child, component.element) - - if dom_id in reverse_store: - id = reverse_store[dom_id] - state = store.get(id) - if state: - component.innerText = state - # Add event listeners - # def isListener(name): - # return name.startswith("on") - # def isAttribute(name): - # return not isListener(name) and name != "children" - - # set attributes - - # for name in props: - # if isAttribute(name): - # dom.setAttribute(name, props[name]) - - # Render children - # children = component.children - # childElements.forEach(childElement => render(childElement, dom)); - # for childElement in children: - # childElement.parentDom = dom - # render(childElement) - - # // Append to parent - # need to apply batch updates here - # also create a tree of dom nodes in the first place - # if parentElement.contains(dom): - # parentElement.replaceChild(dom, parentElement.childNodes[0]) - # else: - # parentElement.appendChild(dom) - - -# the render is the wrong implementation + data = evaluate_data(child, id) + domElement.innerText = data + hydrate(child) def create_signal(initial_state=None): """Create a signal to be used in a component.""" global store, reverse_store - id = random.randint(0, 100000) + signal_id = random.randint(0, 100000) - def use_signal(element=None): # this will be the dom id + def use_signal(dom_id=None): # this will be the dom id """Get the state and manage observers.""" - nonlocal id - if element: - observers.setdefault(id, []).append(element) - reverse_store[element] = id + nonlocal signal_id + if dom_id: + observers.setdefault(signal_id, []).append(dom_id) + reverse_store[dom_id] = signal_id - return store.get(id, initial_state) + return store.get(signal_id, initial_state) - def set_signal(state): - """Set a new state and trigger re-render for observers.""" - """The render process works in a wrong way at this point but works for now.""" - """We need to update the value in the state""" + def set_signal(initial_state=None): # need to fix this # we need to maintain a global tree indexed by uuids after the hydration process # after the hydration process, we need to update the tree with the new state # then we need to re-render the tree - nonlocal id - store[id] = state - for component_id in observers.get(id, []): + nonlocal signal_id + store[signal_id] = initial_state + for component_id in observers.get(signal_id, []): component = js.document.querySelector(f"[data-pyxide-id='{component_id}']") + component = clientDomIdMap.get(component_id) + if component is None: + continue + print("component", component, "new state", initial_state) + print("clientDomIdMap", clientDomIdMap) + js.console.log( "This is the component", component, str(dir(component)), component.children, ) - - render(component, component.parentElement) + component.re_render() + hydrate(component) + print("hydration ended") def get_signal(*args, **kwargs): # args and kwargs are not used but a hack as the ids are being # passed as arguments currently # will be fixed once the serialization and deserialization is done """Get the current state without affecting the observer list.""" - nonlocal id - return store.get(id, initial_state) + nonlocal signal_id + return store.get(signal_id, initial_state) return [use_signal, set_signal, get_signal] diff --git a/starfyre/parser.py b/starfyre/parser.py index 72c0584..29ec659 100644 --- a/starfyre/parser.py +++ b/starfyre/parser.py @@ -184,7 +184,6 @@ def handle_starttag(self, tag, attrs): # logic should be to just create an empty component on start # and fill the contents on the end tag props = {} - state = {} event_listeners = {} self.current_depth += 1 @@ -210,10 +209,9 @@ def handle_starttag(self, tag, attrs): else: # here we need to check if these are functions # or state objects or just regular text - if attr_value in self.local_variables and self.is_state( + if attr_value in self.local_variables( self.local_variables[attr_value] ): - state[attr[0]] = self.local_variables[attr_value] props[attr[0]] = self.local_variables[attr_value]() else: props[attr[0]] = attr[1] @@ -223,7 +221,6 @@ def handle_starttag(self, tag, attrs): component = self.components[tag] component.original_name = tag component.props = {**component.props, **props} - component.state = {**component.state, **state} component.event_listeners = { **component.event_listeners, @@ -247,7 +244,6 @@ def handle_starttag(self, tag, attrs): props=props, children=[], event_listeners=event_listeners, - state=state, js=self.js, css=self.css, uuid=uuid4(), @@ -260,7 +256,6 @@ def handle_starttag(self, tag, attrs): props=props, children=[], event_listeners=event_listeners, - state=state, js="", css="", uuid=uuid4(), @@ -333,6 +328,15 @@ def is_signal(self, str): def inject_uuid(self, signal, uuid): return signal.replace("()", f"('{uuid}')") + def extract_signal(self, str, id): + if not str: + return False + matches = re.match(rf".+\(.+{id}.+\)", str) + if not matches: + raise Exception("Signal not found. Something went wrong.") + + return matches.group(0) + def handle_data(self, data): # this is doing too much # lexing @@ -346,14 +350,13 @@ def handle_data(self, data): matches = re.findall(r"{(.*?)}", data) # parsing starts here - state = {} parent_node = self.stack[-1] uuid = uuid4() component_signal = "" for match in matches: - # match can be a sentece so we will split it + # match can be a sentence so we will split it current_data = None if match in self.local_variables: current_data = self.local_variables[match] @@ -364,10 +367,12 @@ def handle_data(self, data): if self.is_signal(match): new_js = transpile(match) new_js = self.inject_uuid(new_js, uuid) - component_signal = new_js.strip("{").strip("}").strip(";") - print("new js", new_js) - # inject uuid in the signal function call + component_signal = self.extract_signal(new_js, uuid) + # TODO: we need to account for multiple signals e.g. signal1 + signal2 + 1 + print("This is the new js", new_js) + print("This is the component signal", component_signal) + # inject uuid in the signal function call current_data = new_js else: @@ -394,6 +399,11 @@ def handle_data(self, data): if matches: data = data.replace("{", "").replace("}", "") data = data.replace(match, str(current_data)) + elif self.is_signal(current_data): + # we should find the initial state of the signal here + # TODO for the future + data = current_data + print("This is the current data", current_data) if data == "": return @@ -407,16 +417,14 @@ def handle_data(self, data): # this should never be in the parent stack # a text node is a child node as soon as it is created - # add a parent component - # on the wrapper div component + # we are wrapping the text node in a div wrapper_div_component = Component( tag="div", props={}, children=[], event_listeners={}, - state=state, - data=data, + data="", css="", js="", signal="", @@ -430,7 +438,6 @@ def handle_data(self, data): props={}, children=[], event_listeners={}, - state=state, data=data, css="", js="", @@ -463,7 +470,6 @@ def get_root(self): props={}, children=[], event_listeners={}, - state={}, data="", css=self.css, js=self.js, diff --git a/test_application/components/parent.fyre b/test_application/components/parent.fyre index f822ebe..ff3d777 100644 --- a/test_application/components/parent.fyre +++ b/test_application/components/parent.fyre @@ -21,7 +21,7 @@ def ssr_request(): {ssr_request()} - {use_parent_signal()} + {use_parent_signal() + 1} {get_parent_signal()} diff --git a/test_application/pages/__init__.fyre b/test_application/pages/__init__.fyre index 579d258..b912b2f 100644 --- a/test_application/pages/__init__.fyre +++ b/test_application/pages/__init__.fyre @@ -10,6 +10,7 @@ def mocked_request(): [use_parent_signal, set_parent_signal, get_parent_signal] = create_signal(1) def handle_on_click(e): + print("Hello world") signal_value = get_parent_signal() set_parent_signal(signal_value+1) @@ -17,9 +18,9 @@ def handle_on_click(e): from pyscript.js_modules import Fireworks from pyscript import document -container = document.querySelector("[data-pyxide-id='root']") -f = Fireworks.Fireworks.new(container) -f.start() +#container = document.querySelector("[data-pyxide-id='root']") +#f = Fireworks.Fireworks.new(container) +#f.start() ---