Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept for calling a method when an element becomes visible #271

Merged
merged 1 commit into from
Sep 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions django_unicorn/static/unicorn/js/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class Attribute {
this.isTarget = false;
this.isPartial = false;
this.isDirty = false;
this.isVisible = false;
this.isKey = false;
this.isError = false;
this.modifiers = {};
Expand Down Expand Up @@ -45,6 +46,8 @@ export class Attribute {
this.isPartial = true;
} else if (contains(this.name, ":dirty")) {
this.isDirty = true;
} else if (contains(this.name, ":visible")) {
this.isVisible = true;
} else if (this.name === "unicorn:key" || this.name === "u:key") {
this.isKey = true;
} else if (contains(this.name, ":error:")) {
Expand Down
42 changes: 42 additions & 0 deletions django_unicorn/static/unicorn/js/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class Component {
this.modelEls = [];
this.loadingEls = [];
this.keyEls = [];
this.visibilityEls = [];
this.errors = {};
this.return = {};
this.poll = {};
Expand All @@ -53,6 +54,7 @@ export class Component {

this.init();
this.refreshEventListeners();
this.initVisibility();
this.initPolling();

this.callCalls(args.calls);
Expand Down Expand Up @@ -206,6 +208,10 @@ export class Component {
this.keyEls.push(element);
}

if (hasValue(element.visibility)) {
this.visibilityEls.push(element);
}

element.actions.forEach((action) => {
if (this.actionEvents[action.eventType]) {
this.actionEvents[action.eventType].push({ action, element });
Expand Down Expand Up @@ -255,6 +261,42 @@ export class Component {
});
}

/**
* Initializes `visible` elements.
*/
initVisibility() {
if (
typeof window !== "undefined" &&
"IntersectionObserver" in window &&
"IntersectionObserverEntry" in window &&
"intersectionRatio" in window.IntersectionObserverEntry.prototype
) {
this.visibilityEls.forEach((element) => {
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];

if (entry.isIntersecting) {
this.callMethod(
element.visibility.method,
element.visibility.debounceTime,
element.partials,
(err) => {
if (err) {
console.error(err);
}
}
);
}
},
{ threshold: [element.visibility.threshold] }
);

observer.observe(element.el);
});
}
}

/**
* Handles poll errors.
* @param {Error} err Error.
Expand Down
14 changes: 14 additions & 0 deletions django_unicorn/static/unicorn/js/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class Element {
this.actions = [];
this.partials = [];
this.target = null;
this.visibility = {};
this.key = null;
this.events = [];
this.errors = [];
Expand Down Expand Up @@ -97,6 +98,19 @@ export class Element {
} else {
this.partials.push({ target: attribute.value });
}
} else if (attribute.isVisible) {
let threshold = attribute.modifiers.threshold || 0;

if (threshold > 1) {
// Convert the whole number into a percentage
threshold /= 100;
}

this.visibility.method = attribute.value;
this.visibility.threshold = threshold;
this.visibility.debounceTime = attribute.modifiers.debounce
? parseInt(attribute.modifiers.debounce, 10) || 0
: 0;
} else if (attribute.eventType) {
const action = {};
action.name = attribute.value;
Expand Down
4 changes: 4 additions & 0 deletions example/unicorn/components/js.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class JsView(UnicornView):
)
selected_state = ""
select2_datetime = now()
scroll_counter = 0

def call_javascript(self):
self.call("callAlert", "world")
Expand All @@ -30,5 +31,8 @@ def select_state(self, val, idx):
print("select_state called idx", idx)
self.selected_state = val

def increase_counter(self):
self.scroll_counter += 1

class Meta:
javascript_excludes = ("states",)
105 changes: 56 additions & 49 deletions example/unicorn/templates/unicorn/js.html
Original file line number Diff line number Diff line change
@@ -1,68 +1,75 @@
<div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>

<script>
function callAlert(name) {
alert("hello, " + name);
}
<script>
function callAlert(name) {
alert("hello, " + name);
}

var HelloJs = (function() {
var self = {};
var HelloJs = (function () {
var self = {};

self.hello = function(name){
alert("Hello " + name)
}
self.hello = function (name) {
alert("Hello " + name)
}

return self;
}());
</script>
return self;
}());
</script>

<h2>
<button unicorn:click="call_javascript">Component view calls JavaScript</button>
<button unicorn:click="call_javascript_module">Component view calls JavaScript module</button>
</h2>

<div>
<h2>
<button unicorn:click="call_javascript">Component view calls JavaScript</button>
<button unicorn:click="call_javascript_module">Component view calls JavaScript module</button>
<code>javascript_exclude states</code>
</h2>
{% for state in states %}
{{ state }}
{% endfor %}
</div>

<div>
<h2>
<code>javascript_exclude states</code>
</h2>
{% for state in states %}
{{ state }}
{% endfor %}
</div>
<h2>Select2</h2>

<h2>Select2</h2>
<div u:ignore>
<select unicorn:model="selected_state" class="form-control" id="select2-example" required
onchange="Unicorn.call('js', 'select_state', this.value, this.selectedIndex);">
{% for state in states %}
<option value="{{ state }}">{{ state }}</option>
{% endfor %}
</select>

<div u:ignore>
<select unicorn:model="selected_state" class="form-control" id="select2-example" required onchange="Unicorn.call('js', 'select_state', this.value, this.selectedIndex);">
{% for state in states %}
<option value="{{ state }}">{{ state }}</option>
{% endfor %}
</select>
States (in ignored div): {{ states }}
</div>

States (in ignored div): {{ states }}
</div>
selected_state: {{ selected_state }}

selected_state: {{ selected_state }}
<script>
$(document).ready(function () {
$('#select2-example').select2();
});
</script>

<script>
$(document).ready(function() {
$('#select2-example').select2();
});
</script>
<div>
States (not in ignored div): {{ states }}<br />
<button type="submit" u:click="change_states">Change states</button>
</div>

<div>
<input type="text" u:model="select2_datetime" />
<button type="submit" u:click="get_now">Get now</button>
<div>
States (not in ignored div): {{ states }}<br />
<button type="submit" u:click="change_states">Change states</button>
select2_datetime: {{ select2_datetime }}
</div>
</div>

<div>
<input type="text" u:model="select2_datetime" />
<button type="submit" u:click="get_now">Get now</button>
<div>
select2_datetime: {{ select2_datetime }}
</div>
</div>
<div unicorn:key="visibility">
<span unicorn:visible.threshold-25.debounce-1000="increase_counter" unicorn:partial="visibility">
Number of times this span was scrolled into view: {{ scroll_counter }}
</span>
</div>
</div>
43 changes: 43 additions & 0 deletions tests/js/element/visibility.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import test from "ava";
import { getComponent, getElement } from "../utils.js";

test("visible", (t) => {
const html = "<span unicorn:visible='test_function1'></span>";
const element = getElement(html);

const { visibility } = element;
t.is(visibility.method, "test_function1");
t.is(visibility.threshold, 0);
t.is(visibility.debounceTime, 0);
});

test("visible threshold", (t) => {
const html = "<span unicorn:visible.threshold-25='test_function2'></span>";
const element = getElement(html);

const { visibility } = element;
t.is(visibility.method, "test_function2");
t.is(visibility.threshold, 0.25);
t.is(visibility.debounceTime, 0);
});

test("visible debounce", (t) => {
const html = "<span unicorn:visible.debounce-1000='test_function3'></span>";
const element = getElement(html);

const { visibility } = element;
t.is(visibility.method, "test_function3");
t.is(visibility.threshold, 0);
t.is(visibility.debounceTime, 1000);
});

test("visible chained", (t) => {
const html =
"<span unicorn:visible.threshold-50.debounce-2000='test_function4'></span>";
const element = getElement(html);

const { visibility } = element;
t.is(visibility.method, "test_function4");
t.is(visibility.threshold, 0.5);
t.is(visibility.debounceTime, 2000);
});