Skip to content

Commit

Permalink
Change MORPHER settings to follow the existing conventions.
Browse files Browse the repository at this point in the history
  • Loading branch information
adamghill committed Oct 1, 2023
1 parent e782ce6 commit d3f618d
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 72 deletions.
33 changes: 24 additions & 9 deletions django_unicorn/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from warnings import warn

from django.conf import settings

Expand All @@ -8,6 +9,12 @@
SETTINGS_KEY = "UNICORN"
LEGACY_SETTINGS_KEY = f"DJANGO_{SETTINGS_KEY}"

DEFAULT_MORPHER_NAME = "morphdom"
MORPHER_NAMES = (
"morphdom",
"alpine",
)


def get_settings():
unicorn_settings = {}
Expand Down Expand Up @@ -35,18 +42,26 @@ def get_cache_alias():
return get_setting("CACHE_ALIAS", "default")


def get_morpher():
return get_setting("MORPHER", "morphdom")

def get_morpher_settings():
options = get_setting("MORPHER", {"NAME": DEFAULT_MORPHER_NAME})

def get_morpher_options():
options = get_setting("MORPHER_OPTIONS", {})
# Legacy `RELOAD_SCRIPT_ELEMENTS` setting that needs to go to `MORPHER.RELOAD_SCRIPT_ELEMENTS`
reload_script_elements = get_setting("RELOAD_SCRIPT_ELEMENTS")

# Legacy "RELOAD_SCRIPT_ELEMENTS" setting that needs to go to
# MORPHER_OPTIONS["RELOAD_SCRIPT_ELEMENTS"].
reload_script_elements = get_setting("RELOAD_SCRIPT_ELEMENTS", False)
if reload_script_elements:
options["RELOAD_SCRIPT_ELEMENTS"] = True
msg = 'The `RELOAD_SCRIPT_ELEMENTS` setting is deprecated. Use \
`MORPHER["RELOAD_SCRIPT_ELEMENTS"]` instead.'
warn(msg, DeprecationWarning, stacklevel=1)

options["RELOAD_SCRIPT_ELEMENTS"] = reload_script_elements

if not options.get("NAME"):
options["NAME"] = DEFAULT_MORPHER_NAME

morpher_name = options["NAME"]

if not morpher_name or morpher_name not in MORPHER_NAMES:
raise AssertionError(f"Unknown morpher name: {morpher_name}")

return options

Expand Down
25 changes: 18 additions & 7 deletions django_unicorn/static/unicorn/js/morpher.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { MorphdomMorpher } from "./morphers/morphdom.js";
import { AlpineMorpher } from "./morphers/alpine.js";
import { isEmpty } from "./utils.js";

const MORPHER_CLASSES = {
morphdom: MorphdomMorpher,
alpine: AlpineMorpher,
};

export function getMorpher(morpherSettings) {
const morpherName = morpherSettings.NAME;

if (isEmpty(morpherName)) {
throw Error(" Missing morpher name");
}

const MorpherClass = MORPHER_CLASSES[morpherName];

export function getMorpher(morpherName, morpherOptions) {
const MorpherClass = {
morphdom: MorphdomMorpher,
alpine: AlpineMorpher,
}[morpherName];
if (MorpherClass) {
return new MorpherClass(morpherOptions);
return new MorpherClass(morpherSettings);
}
throw Error(`No morpher found for: ${morpherName}`);

throw Error(`Unknown morpher: ${morpherName}`);
}
13 changes: 7 additions & 6 deletions django_unicorn/static/unicorn/js/morphers/alpine.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Alpine morpher requires Alpine to be loaded. Add Alpine and Alpine Morph to your
See https://www.django-unicorn.com/docs/custom-morphers/#alpine for more information.
`);
}

this.options = options;
}

Expand All @@ -21,15 +22,15 @@ See https://www.django-unicorn.com/docs/custom-morphers/#alpine for more informa
key(el) {
if (el.attributes) {
const key =
el.getAttribute("unicorn:key") ||
el.getAttribute("u:key") ||
el.id;
el.getAttribute("unicorn:key") || el.getAttribute("u:key") || el.id;

if (key) {
return key;
}
}
return el.id
}
}

return el.id;
},
};
}
}
4 changes: 2 additions & 2 deletions django_unicorn/static/unicorn/js/morphers/morphdom.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import morphdom from "../morphdom/2.6.1/morphdom.js";


export class MorphdomMorpher {
constructor(options) {
this.options = options;
Expand All @@ -12,6 +11,7 @@ export class MorphdomMorpher {

getOptions() {
const reloadScriptElements = this.options.RELOAD_SCRIPT_ELEMENTS || false;

return {
childrenOnly: false,
// eslint-disable-next-line consistent-return
Expand Down Expand Up @@ -71,6 +71,6 @@ export class MorphdomMorpher {
}
}
},
}
};
}
}
20 changes: 13 additions & 7 deletions django_unicorn/templates/unicorn/scripts.html
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
{% load static %}

{{ MORPHER_OPTIONS|json_script:"unicorn:settings:morpher-options" }}
{{ MORPHER|json_script:"unicorn:settings:morpher" }}

{% if MINIFIED %}
<script src="{% static 'unicorn/js/unicorn.min.js' %}"></script>
<script>
var url = "{% url 'django_unicorn:message' %}";
var morpherOptions = JSON.parse(document.getElementById("unicorn:settings:morpher-options").textContent);
var morpher = getMorpher("{{ MORPHER }}", morpherOptions);
const url = "{% url 'django_unicorn:message' %}";

const morpherSettings = JSON.parse(document.getElementById("unicorn:settings:morpher").textContent);
const morpher = getMorpher(morpherSettings);

Unicorn.init(url, "{{ CSRF_HEADER_NAME }}", "{{ CSRF_COOKIE_NAME }}", morpher);

</script>
{% else %}
<script type="module">
import * as Unicorn from "{% static 'unicorn/js/unicorn.js' %}";
import { getMorpher } from "{% static 'unicorn/js/morpher.js' %}";

// Set Unicorn to the global, so it can be used by components
window.Unicorn = Unicorn;

var url = "{% url 'django_unicorn:message' %}";
var morpherOptions = JSON.parse(document.getElementById("unicorn:settings:morpher-options").textContent);
var morpher = getMorpher("{{ MORPHER }}", morpherOptions);
const url = "{% url 'django_unicorn:message' %}";

const morpherSettings = JSON.parse(document.getElementById("unicorn:settings:morpher").textContent);
const morpher = getMorpher(morpherSettings);

Unicorn.init(url, "{{ CSRF_HEADER_NAME }}", "{{ CSRF_COOKIE_NAME }}", morpher);
</script>
{% endif %}
5 changes: 2 additions & 3 deletions django_unicorn/templatetags/unicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django_unicorn.call_method_parser import InvalidKwargError, parse_kwarg
from django_unicorn.errors import ComponentNotValidError
from django_unicorn.settings import get_morpher, get_morpher_options
from django_unicorn.settings import get_morpher_settings

register = template.Library()

Expand All @@ -33,8 +33,7 @@ def unicorn_scripts():
"MINIFIED": get_setting("MINIFIED", not settings.DEBUG),
"CSRF_HEADER_NAME": csrf_header_name,
"CSRF_COOKIE_NAME": csrf_cookie_name,
"MORPHER": get_morpher(),
"MORPHER_OPTIONS": get_morpher_options(),
"MORPHER": get_morpher_settings(),
}


Expand Down
64 changes: 32 additions & 32 deletions docs/source/custom-morphers.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,60 @@
# Custom Morphers

Morpher is a library used to update specific parts of the DOM element instead of replacing the entire element. This improves performance and maintains the state of unchanged DOM elements, such as the cursor position in an input.
The morpher is a library used to update specific parts of the DOM element instead of replacing the entire element. This improves performance and maintains the state of unchanged DOM elements, such as the cursor position in an input.

The default morpher used in Unicorn is [morphdom](https://github.com/patrick-steele-idem/morphdom). If you don't change any settings, morphdom will be used. However, you can switch to a different morpher by setting the "MORPHER" parameter. Each morpher has its own set of configurable settings, which can be adjusted using the "MORPHER_OPTIONS" parameter in the "UNICORN" setting.
The default morpher used in Unicorn is [`morphdom`](https://github.com/patrick-steele-idem/morphdom). The only alternative morpher available is the [Alpine.js morph plugin](https://alpinejs.dev/plugins/morph).

Currently, the only alternative morpher available is the [Alpine.js Morph Plugin](https://alpinejs.dev/plugins/morph).
## `Morphdom`

## Morphdom
`morphdom` is the default morpher so no extra settings or installation is required to use it.

Since it's the default morpher, and it's built into Unicorn, no extra steps are required to use it.

### Morphdom Settings
### Example settings

```python
# File: settings.py
# settings.py

UNICORN = {
"MORPHER": "morphdom",
"MORHER_OPTIONS": {
"RELOAD_SCRIPT_ELEMENTS": False,
},
...
"MORPHER": {
"NAME": "morphdom",
"RELOAD_SCRIPT_ELEMENTS": True,
}
...
}
```

### Morphdom Options

- **RELOAD_SCRIPT_ELEMENTS** (default is `False`): Whether `script` elements in a template should be "re-run" after a template has been re-rendered.
## `Alpine`

## Alpine
Components which use both `Unicorn` and `Alpine.js` should use the `Alpine.js` morpher to prevent losing state when it gets re-rendered.

The Alpine morpher is helpful if you use Alpine.js and need to keep the Alpine state of your components after a template has been re-rendered.
### Example settings

```python
# settings.py

### Alpine Options
UNICORN = {
...
"MORPHER": {
"NAME": "alpine",
}
...
}
```

The Alpine.js morpher doesn't have any options.
```{note}
`RELOAD_SCRIPT_ELEMENTS` is not currently supported for the `Alpine.js` morpher.
```

### Alpine Installation
### JavaScript Installation

To use the Alpine.js morpher, you need to include Alpine.js and Alpine.js Morpher plugin in your template. You can do this by adding the following line to your template:
`Alpine.js` is not included in `Unicorn` so you will need to manually include it. Make sure to include `Alpine.js` and the morpher plugin by adding the following line to your template before `{% unicorn_scripts %}`.

```html
...
<head>
...
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
{% unicorn_scripts %}
</head>
```

Then switch to the Alpine.js morpher in your settings:

```python
# File: settings.py

UNICORN = {
"MORPHER": "alpine",
}
...
```
16 changes: 11 additions & 5 deletions docs/source/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ UNICORN = {
"CACHE_ALIAS": "default",
"MINIFY_HTML": False,
"MINIFIED": True,
"RELOAD_SCRIPT_ELEMENTS": False,
"SERIAL": {
"ENABLED": False,
"TIMEOUT": 60,
},
"SCRIPT_LOCATION": "after",
"MORPHER": "morphdom",
"MORPHER_OPTIONS": {
"MORPHER": {
"NAME": "morphdom",
"RELOAD_SCRIPT_ELEMENTS": False,
},
}
Expand Down Expand Up @@ -58,7 +57,14 @@ Where the initial JavaScript data is included on initial render. Two values are

**append** will render the JavaScript _inside_ of the HTML component.


## MORPHER

The library to use for diffing and merging the DOM. Defaults to `"morphdom"`. Can be set to "alpine" to use the [Alpine.js Morph Plugin](https://alpinejs.dev/plugins/morph). See [Custom Morphers](custom-morphers.md) for more information.
Configures the library to use for diffing and merging the DOM. Defaults to `{}`.

### NAME

The name of the morpher to use. Defaults to `"morphdom"`. Specify `"alpine"` to use the [Alpine.js Morph Plugin](https://alpinejs.dev/plugins/morph). See [Custom Morphers](custom-morphers.md) for more information.

### RELOAD_SCRIPT_ELEMENTS

Whether script elements should be reloaded when a component is re-rendered. Defaults to `False`. Only available with the `"morphdom"` morpher.
4 changes: 3 additions & 1 deletion example/project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@
"TIMEOUT": 5,
},
"CACHE_ALIAS": "default",
"RELOAD_SCRIPT_ELEMENTS": True,
"MORPHER": {
"RELOAD_SCRIPT_ELEMENTS": True,
},
}


Expand Down
20 changes: 20 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import pytest

from django_unicorn.settings import (
get_cache_alias,
get_minify_html_enabled,
get_morpher_settings,
get_script_location,
get_serial_enabled,
)
Expand Down Expand Up @@ -61,3 +64,20 @@ def test_get_script_location(settings):
del settings.UNICORN["SCRIPT_LOCATION"]

assert get_script_location() == "after"


def test_get_morpher_settings(settings):
assert get_morpher_settings() == {"NAME": "morphdom"}

settings.UNICORN["MORPHER"] = {"NAME": "alpine"}
assert get_morpher_settings()["NAME"] == "alpine"

settings.UNICORN["MORPHER"] = {"NAME": "blob"}

with pytest.raises(AssertionError) as e:
get_morpher_settings()

assert e.type == AssertionError
assert e.exconly() == "AssertionError: Unknown morpher name: blob"

del settings.UNICORN["MORPHER"]

0 comments on commit d3f618d

Please sign in to comment.