Skip to content

Commit

Permalink
Render the tree on the client (#126)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
sansyrox and pre-commit-ci[bot] authored Jan 16, 2024
1 parent 676a97a commit a1fa1a5
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 302 deletions.
9 changes: 9 additions & 0 deletions docs/component-philospy.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion starfyre/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def create_component(
props={},
children=[],
event_listeners={},
state={},
uuid="store",
js=js,
original_name="div",
Expand Down
21 changes: 20 additions & 1 deletion starfyre/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand All @@ -31,3 +33,20 @@ def is_slot_component(self):

def __repr__(self):
return f"<{self.tag}> {self.data} {self.children} </{self.tag}>"

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
29 changes: 9 additions & 20 deletions starfyre/dist_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -98,8 +86,11 @@ def generate_html_pages(file_routes, project_dir: Path):
html_file.write(
"<script type='module' src='https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.js'></script>"
)
html_file.write(
"<script type='mpy'>GLOBAL_STORE={}; GLOBAL_OBSERVERS={}; GLOBAL_REVERSE_OBSERVERS={}; GLOBAL_CLIENT_DOM_ID_MAP={};</script>"
)
html_file.write("<script type='mpy'>print(GLOBAL_STORE)</script>")
html_file.write("<script type='mpy' src='./store.py'></script>")
html_file.write("<script type='mpy' src='./dom_methods.py'></script>")
html_file.write(rendered_page)

# Change back to the original directory
Expand Down Expand Up @@ -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")

Expand Down
86 changes: 54 additions & 32 deletions starfyre/dom_methods.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 = ""
Expand All @@ -47,44 +69,28 @@ def render_helper(component: Component) -> tuple[str, str, str, str]:
props={"id": "root"},
children=[],
event_listeners={},
state={},
uuid=uuid4(),
original_name="div",
)
component.parentComponent = parentElement

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:
Expand Down Expand Up @@ -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
Expand All @@ -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"""<!DOCTYPE html>
<meta charset='UTF-8'>
<script type='mpy' config='pyscript.toml'>{client_side_python}</script>
<style>{css}</style>
<div data-pyxide-id='root'>{html}</div>
<script>{js}</script>"""
<meta charset='UTF-8'>
<script>window["STARFYRE_ROOT_NODE"]=`{tree}`</script>
<script type='mpy' src='./dom_helpers.py'></script>
<script type='mpy' config='pyscript.toml'>{client_side_python}</script>
<style>{css}</style>
<div data-pyxide-id='root'>{html}</div>
<script>{js}</script>"""
return final_html
121 changes: 121 additions & 0 deletions starfyre/js/dom_helpers.py
Original file line number Diff line number Diff line change
@@ -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} </{self.tag}>"

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()
Loading

0 comments on commit a1fa1a5

Please sign in to comment.