Skip to content

Commit

Permalink
Merge pull request #42 from edelvalle/encode-state-in-url
Browse files Browse the repository at this point in the history
Allow components to encode part of their state in the URL
  • Loading branch information
edelvalle committed Jul 3, 2022
2 parents 18cdfdb + faaede9 commit db9c2e1
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 3 deletions.
27 changes: 26 additions & 1 deletion README.md
Expand Up @@ -63,7 +63,8 @@ In the templates where you want to use reactive components you have to load the
<!DOCTYPE html>
<html>
<head>
.... {% reactor_header %}
...
{% reactor_header %}
</head>
...
</html>
Expand Down Expand Up @@ -140,6 +141,29 @@ And the index template being:

Don't forget to update your `urls.py` to call the index view.

### Persisting the state of the Counter in the URL as a GET parameter

Add:

```python
...

class XCounter(Component):
_url_params = {"amount": "counter_amount"} # local attr -> get parameter name

...
```

This will make it so when everytime amount is updated the URL will get [replaced](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState) updating the GET parameter `?&counter_amount=20` (in case counter=20). So the user can copy that URL and share it, or navigate back to it and you can retrieve that GET parameter and restore the state of the component.


```html
...
<body>
{% component 'XCounter' amount=request.GET.counter_amount|default:0 %}
</body>
...
```

## Settings:

Expand Down Expand Up @@ -233,6 +257,7 @@ Instead use the class method `new` to create the instance.
- `_extends`: (default: `"div"`) Tag name HTML element the component extends. (Each component is a HTML5 component so it should extend some HTML tag)
- `_template_name`: Contains the path of the template of the component.
- `_exclude_fields`: (default: `{"user", "reactor"}`) Which fields to exclude from state serialization during rendering
- `_url_params`: (default: `{}`) Indicates which local attribute should be persisted in the URL as a GET parameter, being the key a local attribute name and the value the name of the GET parameter that will contain the value of the local attribute.

##### Caching

Expand Down
2 changes: 2 additions & 0 deletions reactor/component.py
Expand Up @@ -200,6 +200,7 @@ class Component(BaseModel):
_template_name: str = ... # type: ignore
_fqn: str
_tag_name: str
_url_params: t.Mapping[str, str] = {} # local_attr_name -> url_param_name

# HTML tag that this component extends
_extends = "div"
Expand All @@ -220,6 +221,7 @@ class Component(BaseModel):

class Config:
arbitrary_types_allowed = True
validate_assignment = True
json_encoders = { # type: ignore
models.Model: lambda x: serializer.encode(x), # type: ignore
models.QuerySet: lambda qs: [x.pk for x in qs], # type: ignore
Expand Down
5 changes: 5 additions & 0 deletions reactor/consumer.py
Expand Up @@ -138,6 +138,11 @@ async def send_render(self, component):
"render",
{"id": component.id, "diff": diff},
)
if url_params := {
param: getattr(component, attr)
for attr, param in component._url_params.items()
}:
await self.send_command("set_url_params", url_params)

async def send_command(self, command, payload):
await self.send_json({"command": command, "payload": payload})
Expand Down
35 changes: 35 additions & 0 deletions reactor/static/reactor/reactor.js
Expand Up @@ -69,6 +69,41 @@ class ServerConnection extends EventTarget {
boost.HistoryCache.load(url)
}
break
case "set_url_params":
console.log("<< SET URL PARAMS", payload)

// "?a=x&..." -> "a=x&..."
let searchParams = document.location.search.slice(1)

let currentParams = {};
if (searchParams.length) {
currentParams = searchParams
.split("&") // ["a=x", ...]
.map((x) => x.split("=")) // [["a", "x"], ...]
.reduce( // {a: "x", ...}
(acc, data) => {
let [key, value] = data
acc[key] = value === undefined ? undefined : decodeURIComponent(value)
return acc
},
{}
)
}

let newParams = Object
.entries(Object.assign(currentParams, payload))
.map((key_value) => {
let [key, value] = key_value
return key + (value === undefined ? "" : `=${encodeURIComponent(value)}`)
})

let newpath = document.location.pathname
if (newParams.length) {
newpath += "?" + newParams.join("&")
}
boost.HistoryCache.replace(newpath)
break;

case "page":
var { url, content } = payload
console.log("<< PAGE", `"${url}"`)
Expand Down
1 change: 1 addition & 0 deletions tests/fision/todo/live.py
Expand Up @@ -11,6 +11,7 @@
class XTodoList(Component):
_template_name = "todo/list.html"
_subscriptions = {"item"}
_url_params = {"showing": "showing"}

showing: str = "all"
new_item: str = ""
Expand Down
2 changes: 1 addition & 1 deletion tests/fision/todo/templates/todo.html
Expand Up @@ -3,5 +3,5 @@
{% load reactor %}

{% block body %}
{% component 'XTodoList' %}
{% component 'XTodoList' showing=showing %}
{% endblock body %}
9 changes: 8 additions & 1 deletion tests/fision/todo/views.py
Expand Up @@ -7,7 +7,14 @@ def index(request):


def todo(request):
return render(request, "todo.html", context={"title": "todo"})
return render(
request,
"todo.html",
context={
"title": "todo",
"showing": request.GET.get("showing", "all"),
},
)


def redirect_to_index(request):
Expand Down

0 comments on commit db9c2e1

Please sign in to comment.