From 0de6ee18ec76aa6a5759664b7f573978f5de7c86 Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Wed, 25 Aug 2021 23:02:54 +0100 Subject: [PATCH 01/15] make template library available in test templates --- conftest.py | 5 +++++ tests/templates/test_component_parent.html | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 2646fdb3..3283d569 100644 --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,11 @@ def pytest_configure(): { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["tests"], + "OPTIONS": { + "libraries": { + "unicorn": "django_unicorn.templatetags.unicorn", + } + }, } ] databases = {"default": {"ENGINE": "django.db.backends.sqlite3",}} diff --git a/tests/templates/test_component_parent.html b/tests/templates/test_component_parent.html index e207cfed..853d0f3c 100644 --- a/tests/templates/test_component_parent.html +++ b/tests/templates/test_component_parent.html @@ -1,4 +1,6 @@ +{% load unicorn %} +
parent - {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view } + {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentKwargs' parent=view %}
\ No newline at end of file From e04be1808da73288e470eb55225010e99a69f0d5 Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Wed, 25 Aug 2021 23:11:33 +0100 Subject: [PATCH 02/15] add basic test for render 3 layers of components --- .../templates/test_component_parent_nested.html | 6 ++++++ tests/templatetags/test_unicorn_render.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/templates/test_component_parent_nested.html diff --git a/tests/templates/test_component_parent_nested.html b/tests/templates/test_component_parent_nested.html new file mode 100644 index 00000000..b9a2cf7b --- /dev/null +++ b/tests/templates/test_component_parent_nested.html @@ -0,0 +1,6 @@ +{% load unicorn %} + +
+ parent nested (3 layers) + {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent' parent=view %} +
\ No newline at end of file diff --git a/tests/templatetags/test_unicorn_render.py b/tests/templatetags/test_unicorn_render.py index f112cea3..ea76b698 100644 --- a/tests/templatetags/test_unicorn_render.py +++ b/tests/templatetags/test_unicorn_render.py @@ -46,6 +46,10 @@ def mount(self): self.call("testCall2", "hello") +class FakeComponentParentNested(UnicornView): + template_name = "templates/test_component_parent_nested.html" + + def test_unicorn_render_kwarg(): token = Token( TokenType.TEXT, @@ -303,3 +307,15 @@ def test_unicorn_render_hash(settings): rendered_content = html[:script_idx] expected_hash = generate_checksum(rendered_content) assert f'"hash":"{expected_hash}"' in html + + +def test_unicorn_render_parent_nested_multiple_layers(settings): + settings.DEBUG = True + token = Token( + TokenType.TEXT, + "unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParentNested'", + ) + unicorn_node = unicorn(None, token) + context = {} + html = unicorn_node.render(context) + assert html.count("componentInit") == 3 \ No newline at end of file From bf1708742491f5b9c9d83756187a85155b3b3bba Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Thu, 26 Aug 2021 22:38:25 +0100 Subject: [PATCH 03/15] always include script tags from child components, defer if parent is set --- .../components/unicorn_template_response.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/django_unicorn/components/unicorn_template_response.py b/django_unicorn/components/unicorn_template_response.py index 09c342d4..09008ea2 100644 --- a/django_unicorn/components/unicorn_template_response.py +++ b/django_unicorn/components/unicorn_template_response.py @@ -95,17 +95,18 @@ def render(self): json_tag["id"] = json_element_id json_tag.string = sanitize_html(init) + # Include init script and json tags from child components + json_tags = [json_tag] + for child in self.component.children: + init_script = f"{init_script} {child._init_script}" + json_tags.extend(child._json_tags) + + # Defer rendering the init script and json tag until the outermost + # component (without a parent) is rendered if self.component.parent: self.component._init_script = init_script - self.component._json_tag = json_tag + self.component._json_tags = json_tags else: - json_tags = [] - json_tags.append(json_tag) - - for child in self.component.children: - init_script = f"{init_script} {child._init_script}" - json_tags.append(child._json_tag) - script_tag = soup.new_tag("script") script_tag["type"] = "module" script_tag.string = f"if (typeof Unicorn === 'undefined') {{ console.error('Unicorn is missing. Do you need {{% load unicorn %}} or {{% unicorn_scripts %}}?') }} else {{ {init_script} }}" From f75fc9cb4f8922b9960dc332db82fbdadb3e0d5e Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Thu, 26 Aug 2021 23:21:08 +0100 Subject: [PATCH 04/15] check child has deferred attrs before adding them --- django_unicorn/components/unicorn_template_response.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/django_unicorn/components/unicorn_template_response.py b/django_unicorn/components/unicorn_template_response.py index 09008ea2..5870abfa 100644 --- a/django_unicorn/components/unicorn_template_response.py +++ b/django_unicorn/components/unicorn_template_response.py @@ -98,8 +98,10 @@ def render(self): # Include init script and json tags from child components json_tags = [json_tag] for child in self.component.children: - init_script = f"{init_script} {child._init_script}" - json_tags.extend(child._json_tags) + if hasattr(child, "_init_script"): + init_script = f"{init_script} {child._init_script}" + if hasattr(child, "_json_tags"): + json_tags.extend(child._json_tags) # Defer rendering the init script and json tag until the outermost # component (without a parent) is rendered From ea3adb94ff4d099aa96ea6af798f39941a0c1aef Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Thu, 12 Aug 2021 23:06:44 -0400 Subject: [PATCH 05/15] Bump to 0.33.0. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3562fb48..21c3e3f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-unicorn" -version = "0.32.0" +version = "0.33.0" description = "A magical full-stack framework for Django." authors = ["Adam Hill "] license = "MIT" From 1d60a29e26c704294435cef90ddeab02c32b514c Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Sun, 22 Aug 2021 17:06:48 +0100 Subject: [PATCH 06/15] add optional filter arg to utils.walk and two filters filter implements NodeFilter interface, default is FilterAny --- django_unicorn/static/unicorn/js/utils.js | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/django_unicorn/static/unicorn/js/utils.js b/django_unicorn/static/unicorn/js/utils.js index d26c56bc..745ea4a4 100644 --- a/django_unicorn/static/unicorn/js/utils.js +++ b/django_unicorn/static/unicorn/js/utils.js @@ -104,14 +104,34 @@ export function toKebabCase(str) { return match.map((x) => x.toLowerCase()).join("-"); } +/** + * Filter to accept any element (use with walk) + */ + export const FilterAny = { + acceptNode: (node) => NodeFilter.FILTER_ACCEPT +} + +/** + * Filter to skip nested components (use with walk) + */ +export const FilterSkipNested = { + acceptNode: (node) => { + if (node.getAttribute("unicorn:checksum")) { + // with a tree walker, child nodes are also rejected + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + } +} + /** * Traverses the DOM looking for child elements. */ -export function walk(el, callback) { +export function walk(el, callback, filter=FilterAny) { const walker = document.createTreeWalker( el, NodeFilter.SHOW_ELEMENT, - null, + filter, false ); From 9021281742f2d8e88066086c5dd8ab7566bdad7c Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Sun, 22 Aug 2021 17:11:09 +0100 Subject: [PATCH 07/15] use filter to skip nested components in refreshEventListeners --- django_unicorn/static/unicorn/js/component.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/django_unicorn/static/unicorn/js/component.js b/django_unicorn/static/unicorn/js/component.js index fadedb1a..24e65283 100644 --- a/django_unicorn/static/unicorn/js/component.js +++ b/django_unicorn/static/unicorn/js/component.js @@ -8,7 +8,7 @@ import { import { components } from "./store.js"; import { send } from "./messageSender.js"; import morphdom from "./morphdom/2.6.1/morphdom.js"; -import { $, hasValue, isEmpty, isFunction, walk } from "./utils.js"; +import { $, hasValue, isEmpty, isFunction, walk, FilterSkipNested } from "./utils.js"; /** * Encapsulate component. @@ -169,10 +169,6 @@ export class Component { // Skip the component root element return; } - if (el.getAttribute("unicorn:checksum")) { - // Skip nested components - throw Error(); - } const element = new Element(el); @@ -240,7 +236,7 @@ export class Component { } }); } - }); + }, FilterSkipNested); } catch (err) { // nothing } From c57aa5db6539c9f2f708b3d1d00bbda95f14bcfe Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Sun, 22 Aug 2021 17:38:52 -0400 Subject: [PATCH 08/15] Slight formatting changes. --- django_unicorn/static/unicorn/js/component.js | 135 ++++++++++-------- django_unicorn/static/unicorn/js/utils.js | 14 +- 2 files changed, 80 insertions(+), 69 deletions(-) diff --git a/django_unicorn/static/unicorn/js/component.js b/django_unicorn/static/unicorn/js/component.js index 24e65283..37fa43c7 100644 --- a/django_unicorn/static/unicorn/js/component.js +++ b/django_unicorn/static/unicorn/js/component.js @@ -8,7 +8,14 @@ import { import { components } from "./store.js"; import { send } from "./messageSender.js"; import morphdom from "./morphdom/2.6.1/morphdom.js"; -import { $, hasValue, isEmpty, isFunction, walk, FilterSkipNested } from "./utils.js"; +import { + $, + hasValue, + isEmpty, + isFunction, + walk, + FilterSkipNested, +} from "./utils.js"; /** * Encapsulate component. @@ -164,79 +171,83 @@ export class Component { this.dbEls = []; try { - this.walker(this.root, (el) => { - if (el.isSameNode(this.root)) { - // Skip the component root element - return; - } + this.walker( + this.root, + (el) => { + if (el.isSameNode(this.root)) { + // Skip the component root element + return; + } - const element = new Element(el); + const element = new Element(el); - if (element.isUnicorn) { - if (hasValue(element.field) && hasValue(element.db)) { - if (!this.attachedDbEvents.some((e) => e.isSame(element))) { - this.attachedDbEvents.push(element); - addDbEventListener(this, element); + if (element.isUnicorn) { + if (hasValue(element.field) && hasValue(element.db)) { + if (!this.attachedDbEvents.some((e) => e.isSame(element))) { + this.attachedDbEvents.push(element); + addDbEventListener(this, element); - // If a field is lazy, also add an event listener for input for dirty states - if (element.field.isLazy) { - // This input event for isLazy will be stopped after dirty is checked when the event fires - addDbEventListener(this, element, "input"); + // If a field is lazy, also add an event listener for input for dirty states + if (element.field.isLazy) { + // This input event for isLazy will be stopped after dirty is checked when the event fires + addDbEventListener(this, element, "input"); + } } - } - if (!this.dbEls.some((e) => e.isSame(element))) { - this.dbEls.push(element); - } - } else if ( - hasValue(element.model) && - isEmpty(element.db) && - isEmpty(element.field) - ) { - if (!this.attachedModelEvents.some((e) => e.isSame(element))) { - this.attachedModelEvents.push(element); - addModelEventListener(this, element); - - // If a model is lazy, also add an event listener for input for dirty states - if (element.model.isLazy) { - // This input event for isLazy will be stopped after dirty is checked when the event fires - addModelEventListener(this, element, "input"); + if (!this.dbEls.some((e) => e.isSame(element))) { + this.dbEls.push(element); + } + } else if ( + hasValue(element.model) && + isEmpty(element.db) && + isEmpty(element.field) + ) { + if (!this.attachedModelEvents.some((e) => e.isSame(element))) { + this.attachedModelEvents.push(element); + addModelEventListener(this, element); + + // If a model is lazy, also add an event listener for input for dirty states + if (element.model.isLazy) { + // This input event for isLazy will be stopped after dirty is checked when the event fires + addModelEventListener(this, element, "input"); + } } - } - if (!this.modelEls.some((e) => e.isSame(element))) { - this.modelEls.push(element); - } - } else if (hasValue(element.loading)) { - this.loadingEls.push(element); + if (!this.modelEls.some((e) => e.isSame(element))) { + this.modelEls.push(element); + } + } else if (hasValue(element.loading)) { + this.loadingEls.push(element); - // Hide loading elements that are shown when an action happens - if (element.loading.show) { - element.hide(); + // Hide loading elements that are shown when an action happens + if (element.loading.show) { + element.hide(); + } } - } - if (hasValue(element.key)) { - this.keyEls.push(element); - } - - element.actions.forEach((action) => { - if (this.actionEvents[action.eventType]) { - this.actionEvents[action.eventType].push({ action, element }); - } else { - this.actionEvents[action.eventType] = [{ action, element }]; + if (hasValue(element.key)) { + this.keyEls.push(element); + } - if ( - !this.attachedEventTypes.some((et) => et === action.eventType) - ) { - this.attachedEventTypes.push(action.eventType); - addActionEventListener(this, action.eventType); - element.events.push(action.eventType); + element.actions.forEach((action) => { + if (this.actionEvents[action.eventType]) { + this.actionEvents[action.eventType].push({ action, element }); + } else { + this.actionEvents[action.eventType] = [{ action, element }]; + + if ( + !this.attachedEventTypes.some((et) => et === action.eventType) + ) { + this.attachedEventTypes.push(action.eventType); + addActionEventListener(this, action.eventType); + element.events.push(action.eventType); + } } - } - }); - } - }, FilterSkipNested); + }); + } + }, + FilterSkipNested + ); } catch (err) { // nothing } diff --git a/django_unicorn/static/unicorn/js/utils.js b/django_unicorn/static/unicorn/js/utils.js index 745ea4a4..56566614 100644 --- a/django_unicorn/static/unicorn/js/utils.js +++ b/django_unicorn/static/unicorn/js/utils.js @@ -105,11 +105,11 @@ export function toKebabCase(str) { } /** - * Filter to accept any element (use with walk) + * Filter to accept any element (use with walk) */ - export const FilterAny = { - acceptNode: (node) => NodeFilter.FILTER_ACCEPT -} +export const FilterAny = { + acceptNode: (node) => NodeFilter.FILTER_ACCEPT, +}; /** * Filter to skip nested components (use with walk) @@ -121,13 +121,13 @@ export const FilterSkipNested = { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; - } -} + }, +}; /** * Traverses the DOM looking for child elements. */ -export function walk(el, callback, filter=FilterAny) { +export function walk(el, callback, filter = FilterAny) { const walker = document.createTreeWalker( el, NodeFilter.SHOW_ELEMENT, From 6d333254ca0344552069738188d575fa9b491b9e Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Sun, 22 Aug 2021 17:39:11 -0400 Subject: [PATCH 09/15] Add button below nested component. --- example/unicorn/templates/unicorn/nested/table.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/unicorn/templates/unicorn/nested/table.html b/example/unicorn/templates/unicorn/nested/table.html index 1b5f1085..055d0202 100644 --- a/example/unicorn/templates/unicorn/nested/table.html +++ b/example/unicorn/templates/unicorn/nested/table.html @@ -32,4 +32,6 @@

{{ name }}

{% endfor %} + + From 5156374d3f2ca9a0e53fbcd600560597c1e140c2 Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Sun, 22 Aug 2021 17:39:25 -0400 Subject: [PATCH 10/15] Add skipped test that is incomplete. --- tests/js/utils.js | 9 ++++++ tests/js/utils/walk.test.js | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/js/utils/walk.test.js diff --git a/tests/js/utils.js b/tests/js/utils.js index 7f296eca..ff39ba73 100644 --- a/tests/js/utils.js +++ b/tests/js/utils.js @@ -3,6 +3,15 @@ import fetchMock from "fetch-mock"; import { Element } from "../../django_unicorn/static/unicorn/js/element.js"; import { Component } from "../../django_unicorn/static/unicorn/js/component.js"; +/** + * Mock some browser globals using a fake DOM + */ +export function setBrowserMocks() { + const dom = new JSDOM("
"); + global.document = dom.window.document; + global.NodeFilter = dom.window.NodeFilter; +} + /** * Gets a fake DOM document based on the passed-in HTML fragement. * @param {String} html HTML fragment. diff --git a/tests/js/utils/walk.test.js b/tests/js/utils/walk.test.js new file mode 100644 index 00000000..88ff7e8c --- /dev/null +++ b/tests/js/utils/walk.test.js @@ -0,0 +1,62 @@ +import test from "ava"; +import { + FilterAny, + FilterSkipNested, + walk, +} from "../../../django_unicorn/static/unicorn/js/utils"; +import { getEl, setBrowserMocks } from "../utils.js"; + +// makes a document and NodeFilter available from a fake DOM +setBrowserMocks(); + +test("walk any", (t) => { + const componentRootHtml = ` +
+ +
+ +
+ +
+ `; + const componentRoot = getEl(componentRootHtml); + const nodes = []; + + walk(componentRoot, (node) => nodes.push(node), FilterAny); + + t.is(nodes.length, 4); + t.is(nodes[0].getAttribute("id"), "name"); + t.is(nodes[1].getAttribute("unicorn:id"), "5jypjiyb:nested.filter"); + t.is(nodes[2].getAttribute("id"), "search"); + t.is(nodes[3].getAttribute("id"), "name2"); +}); + +test("walk skip nested", (t) => { + const componentRootHtml = ` +
+ +
+ +
+ +
+ `; + const componentRoot = getEl(componentRootHtml); + const nodes = []; + + walk(componentRoot, (node) => nodes.push(node), FilterSkipNested); + + t.is(nodes.length, 2); + t.is(nodes[0].getAttribute("id"), "name"); + t.is(nodes[1].getAttribute("id"), "name2"); +}); From 69798d3ecf51241b75dd0d800b33ff1ca0f2e538 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 28 Aug 2021 01:21:31 +0000 Subject: [PATCH 11/15] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a689985..e029c398 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![Sponsors](https://www.sponsorama.dev/api/button/sponsor-count/adamghill) -[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-) [Unicorn](https://www.django-unicorn.com) is a reactive component framework that progressively enhances a normal Django view, makes AJAX calls in the background, and dynamically updates the DOM. It seamlessly extends Django past its server-side framework roots without giving up all of its niceties or re-building your website. @@ -47,6 +47,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Ron

📖
Franziskhan

💻 +
Josh Higgins

⚠️ 💻 From 0d062b799bea6ad4a91ae17a8d83e0d34e4fa215 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sat, 28 Aug 2021 01:21:32 +0000 Subject: [PATCH 12/15] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 9bd6c8b8..5d27cb7b 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -87,6 +87,16 @@ "contributions": [ "code" ] + }, + { + "login": "joshiggins", + "name": "Josh Higgins", + "avatar_url": "https://avatars.githubusercontent.com/u/5124298?v=4", + "profile": "https://github.com/joshiggins", + "contributions": [ + "test", + "code" + ] } ], "contributorsPerLine": 7, From 655aeac8f1ae81b42cb25248d7a280bc728ec141 Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Fri, 27 Aug 2021 21:25:00 -0400 Subject: [PATCH 13/15] Add more urls. --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 21c3e3f3..325493fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,12 @@ license = "MIT" readme = "README.md" repository = "https://github.com/adamghill/django-unicorn/" homepage = "https://www.django-unicorn.com" +documentation = "https://www.django-unicorn.com/docs/" keywords = ["django", "python", "javascript", "fullstack"] +[tool.poetry.urls] +"Funding" = "https://github.com/sponsors/adamghill" + [tool.poetry.dependencies] python = "^3.6" django = ">=2.2" From d4044b9496dcda319fa381fd9d2d83e7aa0a4360 Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Fri, 27 Aug 2021 22:07:18 -0400 Subject: [PATCH 14/15] Add example for deeply nested components. --- conftest.py | 4 +--- example/unicorn/components/nested/actions.py | 21 +++++++++++++++++++ example/unicorn/components/nested/row.py | 14 ++++++------- .../templates/unicorn/nested/actions.html | 8 +++++++ .../unicorn/templates/unicorn/nested/row.html | 11 +++------- 5 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 example/unicorn/components/nested/actions.py create mode 100644 example/unicorn/templates/unicorn/nested/actions.html diff --git a/conftest.py b/conftest.py index 3283d569..103104a1 100644 --- a/conftest.py +++ b/conftest.py @@ -9,9 +9,7 @@ def pytest_configure(): "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["tests"], "OPTIONS": { - "libraries": { - "unicorn": "django_unicorn.templatetags.unicorn", - } + "libraries": {"unicorn": "django_unicorn.templatetags.unicorn",} }, } ] diff --git a/example/unicorn/components/nested/actions.py b/example/unicorn/components/nested/actions.py new file mode 100644 index 00000000..a82233b8 --- /dev/null +++ b/example/unicorn/components/nested/actions.py @@ -0,0 +1,21 @@ +from django_unicorn.components import UnicornView +from example.coffee.models import Flavor + + +class ActionsView(UnicornView): + model: Flavor = None + is_editing = False + + def edit(self): + self.is_editing = True + + # this doesn't do what you expect because resulting dom is scoped to + # this component and the parent component won't get morphed + self.parent.is_editing = True + + def cancel(self): + self.is_editing = False + + def save(self): + self.model.save() + self.is_editing = False diff --git a/example/unicorn/components/nested/row.py b/example/unicorn/components/nested/row.py index bf31a7c9..5c7b9c42 100644 --- a/example/unicorn/components/nested/row.py +++ b/example/unicorn/components/nested/row.py @@ -6,12 +6,12 @@ class RowView(UnicornView): model: Flavor = None is_editing = False - def edit(self): - self.is_editing = True + # def edit(self): + # self.is_editing = True - def cancel(self): - self.is_editing = False + # def cancel(self): + # self.is_editing = False - def save(self): - self.model.save() - self.is_editing = False + # def save(self): + # self.model.save() + # self.is_editing = False diff --git a/example/unicorn/templates/unicorn/nested/actions.html b/example/unicorn/templates/unicorn/nested/actions.html new file mode 100644 index 00000000..b6a2746e --- /dev/null +++ b/example/unicorn/templates/unicorn/nested/actions.html @@ -0,0 +1,8 @@ + + {% if is_editing %} + + + {% else %} + + {% endif %} + \ No newline at end of file diff --git a/example/unicorn/templates/unicorn/nested/row.html b/example/unicorn/templates/unicorn/nested/row.html index 97cfe565..b015918f 100644 --- a/example/unicorn/templates/unicorn/nested/row.html +++ b/example/unicorn/templates/unicorn/nested/row.html @@ -1,3 +1,5 @@ +{% load unicorn %} + {% if is_editing %} @@ -22,12 +24,5 @@ n/a {% endif %} - - {% if is_editing %} - - - {% else %} - - {% endif %} - + {% unicorn 'nested.actions' parent=view key=model.id model=model is_editing=is_editing %} \ No newline at end of file From 1dc2c6a23a2a10ef24c735b8fd88c27269bdd80a Mon Sep 17 00:00:00 2001 From: Josh Higgins Date: Thu, 16 Sep 2021 15:11:55 +0100 Subject: [PATCH 15/15] update example based on kwargs proposal --- example/unicorn/components/nested/actions.py | 27 +++++++---------- example/unicorn/components/nested/row.py | 29 ++++++++++++++----- .../templates/unicorn/nested/actions.html | 7 +++-- .../unicorn/templates/unicorn/nested/row.html | 10 ++++++- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/example/unicorn/components/nested/actions.py b/example/unicorn/components/nested/actions.py index a82233b8..43fbcd2e 100644 --- a/example/unicorn/components/nested/actions.py +++ b/example/unicorn/components/nested/actions.py @@ -1,21 +1,14 @@ from django_unicorn.components import UnicornView -from example.coffee.models import Flavor +from typing import Callable class ActionsView(UnicornView): - model: Flavor = None - is_editing = False - - def edit(self): - self.is_editing = True - - # this doesn't do what you expect because resulting dom is scoped to - # this component and the parent component won't get morphed - self.parent.is_editing = True - - def cancel(self): - self.is_editing = False - - def save(self): - self.model.save() - self.is_editing = False + # In this example the ActionsView is completely controlled by the + # RowView and it doesn't really "own" these - its useful to put them + # on the class for type hints and/or default values, but we don't + # want to give the impression they are ActionsView's own state + + is_editing: bool + on_edit: Callable + on_cancel: Callable + on_save: Callable \ No newline at end of file diff --git a/example/unicorn/components/nested/row.py b/example/unicorn/components/nested/row.py index 5c7b9c42..c119aa31 100644 --- a/example/unicorn/components/nested/row.py +++ b/example/unicorn/components/nested/row.py @@ -2,16 +2,31 @@ from example.coffee.models import Flavor +def callback(func): + """A decorator for callbacks passed as kwargs to a child component + + This allows the bound method itself to be resolved in the template. + Without it, the template variable resolver will automatically call + the method and use what is returned as the resolved value. + """ + func.do_not_call_in_templates = True + return func + + class RowView(UnicornView): model: Flavor = None is_editing = False - # def edit(self): - # self.is_editing = True + @callback + def on_edit(self): + print("on_edit callback fired") + self.is_editing = True - # def cancel(self): - # self.is_editing = False + @callback + def on_cancel(self): + self.is_editing = False - # def save(self): - # self.model.save() - # self.is_editing = False + @callback + def on_save(self): + self.model.save() + self.is_editing = False \ No newline at end of file diff --git a/example/unicorn/templates/unicorn/nested/actions.html b/example/unicorn/templates/unicorn/nested/actions.html index b6a2746e..ca8e964c 100644 --- a/example/unicorn/templates/unicorn/nested/actions.html +++ b/example/unicorn/templates/unicorn/nested/actions.html @@ -1,8 +1,9 @@ {% if is_editing %} - - + + + {% else %} - + {% endif %} \ No newline at end of file diff --git a/example/unicorn/templates/unicorn/nested/row.html b/example/unicorn/templates/unicorn/nested/row.html index b015918f..b65892c0 100644 --- a/example/unicorn/templates/unicorn/nested/row.html +++ b/example/unicorn/templates/unicorn/nested/row.html @@ -24,5 +24,13 @@ n/a {% endif %} - {% unicorn 'nested.actions' parent=view key=model.id model=model is_editing=is_editing %} + + {% unicorn 'nested.actions' parent=view key=model.id is_editing=is_editing on_edit=on_edit on_cancel=on_cancel on_save=on_save %} + + + \ No newline at end of file