vjs-reactivity
v1.0.24
Published
Create bindings between a JS variable and a DOM element. Also allows reactivity when a bound JS variable or DOM element changes its value.
Downloads
8
Maintainers
Readme
vjs-reactivity
This simple library is an interface to allow you to interact with your DOM elements as JS variables using vanilla javascript.
Installing
The quickest way to install this library is to use the unpkg cdn.
<script src="https://unpkg.com/vjs-reactivity/dist/bundle.js"></script>
<script>
const { Binding, $$, reactive, claritive } = vjs_reactivity;
</script>
Examples
Here are a couple of examples of how this library can be used.
Simple binding
Simple bindings are ways to interact with your DOM elements as a JS variable by referencing .value
on your bound variable. This example shows how you can create a binding that maintains its value from the DOM element initial value.
<input type="number" id="some-number" value="10">
const x = new Binding("#some-number");
x.value = 12;
console.log(document.querySelector("#some-number").value); // outputs: "12"
Simple binding with initializer
This example shows how you can create a binding that gives an initial value when the binding is created.
<input type="number" id="some-number" value="10">
const x = new Binding("#some-number", 12);
console.log(document.querySelector("#some-number").value); // outputs: "15"
x.value = 15;
console.log(document.querySelector("#some-number").value); // outputs: "12"
Bindings with different triggers
All bindings created have two default triggers: change
and input
. However, sometimes you may want your bindings to only change on a certain trigger. For example, if you are validating an input for percentage and only want it to appear as some number between 0 and 1, having the trigger of input
would be problematic.
You can change this by adding in a third and last argument to your Binding constructor. This argument is an array of strings where each string is the type of event trigger.
Here is an example using the percentage validation as mentioned:
const bX = new Binding("#percent", 0, ["change"]);
$$(bX, (x) => {
// using $value avoids infinite invalidation. You can read more about this below in the "Reactivity and Reassigning Back" section.
bX.$value = x > 1 ? x / 100 : x; // tada! it works!
});
Claritive binding
Claritive bindings are intended to be used when you want to store your bound variables in an object. This will eliminate the need to use .value
to access your bound variable, and instead just reference the variable itself. Here is an example of a claritive binding.
<input type="number" id="some-number-x">
<input type="number" id="some-number-y">
const o = claritive({
x: new Binding("#some-number-x", 5),
y: new Binding("#some-number-y", 10)
});
console.log(document.querySelector("#some-number-x").value); // outputs: "5"
o.x = 15;
console.log(document.querySelector("#some-number-x").value); // outputs: "15"
Accessing the proxy in a claritive binding
Sometimes, you may actually want to get the Binding object itself, instead of just the value that is held by the binding. TypeScript will show you that you have access to all of your properties, plus n more (where n is the number of initial properties you had) prepended with $
. Referencing a property with $
prepended will instead give you the Binding object.
NOTE: Because of this, the properties you give to your object in a claritive binding should not be prepended with $
. An error will be thrown if this happens.
Here is an example of referencing the Binding object instead of the value.
<input type="number" id="some-number-x">
<input type="number" id="some-number-y">
const o = claritive({
x: new Binding("#some-number-x", 5),
y: new Binding("#some-number-y", 10)
});
console.log(o.$x); // outputs: "Binding { ... }".
Reactivity
With these bindings, you can create reactive events that fire when certain bindings change. This can be used to easily make a reactive web application. You can create reactivity by using the function reactive
exported from this module. (Another synonym would be $$
) The arguments passed into reactive
are Binding objects or anonymous functions. All binding objects passed are used as a reference. If the binding changes, then all Functions that were passed in will trigger. The functions that you pass in will take n arguments (where n is the number of Bindings passed in). Each argument will read from left to right in order of what the bindings were passed in as. These arguments will be the newest values from the latest change.
Here is an example of using reactivity.
<input type="number" id="some-number-x">
<input type="number" id="some-number-y">
const o = claritive({
x: new Binding("#some-number-x", 5),
y: new Binding("#some-number-y", 10)
});
// since the binding itself needs to be passed in, we need to use the `$_` extension of the property created from claritive.
reactive(o.$x, (x) => {
o.y = x + 5;
});
console.log(document.querySelector("#some-number-x").value); // outputs: "5"
console.log(document.querySelector("#some-number-y").value); // outputs: "10"
o.x = 10;
console.log(document.querySelector("#some-number-x").value); // outputs: "10"
console.log(document.querySelector("#some-number-y").value); // outputs: "15"
Above was an example using a claritive
binding. Here is an example of just two simple Bindings and using the $$
synonym.
const bX = new Binding("#some-number-x", 5);
const bY = new Binding("#some-number-y", 10);
$$(bX, (x) => {
bY.value = x + 5;
});
console.log(document.querySelector("#some-number-x").value); // outputs: "5"
console.log(document.querySelector("#some-number-y").value); // outputs: "10"
bX.value = 10;
console.log(document.querySelector("#some-number-x").value); // outputs: "10"
console.log(document.querySelector("#some-number-y").value); // outputs: "15"
NOTE: Avoid using reactive statements that create a circular reference.
Reactivity and Reassigning back
As noted in the previous section, you want to avoid circular references, but what if you want to assign your variable back as a form of validation?
Here's an example of what might be applicable, where you, the user, would want to make sure a number passed in as a percentage is actually a number between 0 and 1.
const bX = new Binding("#percent");
$$(bX, (x) => {
bX.value = x > 1 ? x / 100 : x; // error
});
There's just one problem with this-- When you assign bX.value
inside of the reactive statement, you get a Maximum Call Size Exceeded
error, since the reactive statement is infinitely called.
To avoid this infinite invalidation, you can use .$value
to change the value without invalidating the binding. It may not be intuitive, but it gets the job done.
Here is the same example using .$value
instead to avoid invalidation.
const bX = new Binding("#percent", 0, ["change"]);
$$(bX, (x) => {
bX.$value = x > 1 ? x / 100 : x; // tada! it works!
});
Manual invalidation
Above, I explained how you can avoid invalidation, but sometimes you may want to invalidate a variable without changing the value.
You can simply do this by either reassigning the value back to itself, or use the built-in .invalidate()
function.
Here is an example of calculating a net income only when a button is clicked using .invalidate()
.
const bIncome = new Binding("#income");
const bTaxRate = new Binding("#tax-rate");
const bNet = new Binding("#net");
$$(bTaxRate, (percent) => {
bTaxRate.$value = percent > 1 ? percent / 100 : percent;
bIncome.value = bIncome.value * percent;
});
$$(bIncome, (inc) => {
bNet.value = bIncome.value * bTaxRate.value;
});
const btn = document.querySelector("#calculate");
btn.addEventListener("click", () => {
bIncome.invalidate(); // now the bNet will only ever be calculated when the "#net" button is clicked, instead of when bIncome changes.
});
Full example
Here is a full example with the application being a calculator for income/expenses
<script src="https://unpkg.com/vjs-reactivity/dist/bundle.js"></script>
<div>
<h3>Income</h3>
<label>Total Income: <input type="number" id="amount"></label>
<select id="income-basis">
<option value="daily">daily</option>
<option value="weekly">weekly</option>
<option value="bi-weekly">bi-weekly</option>
<option value="monthly">monthly</option>
<option value="yearly">yearly</option>
</select>
</div>
<div>
<h3>Expenses</h3>
<label>Groceries: <input type="number" id="groceries" value="300"></label>
<label>Bills: <input type="number" id="bills" value="1000"></label>
<label>Entertainment: <input type="number" id="entertainment" value="200"></label>
<select id="expenses-basis">
<option value="daily">daily</option>
<option value="weekly">weekly</option>
<option value="bi-weekly">bi-weekly</option>
<option value="monthly">monthly</option>
<option value="yearly">yearly</option>
</select>
</div>
<div>
<h3>Outcome</h3>
<label>Per day: <input type="number" id="result-per-day" disabled="disabled"></label>
<label>Per week: <input type="number" id="result-per-week" disabled="disabled"></label>
<label>Per month: <input type="number" id="result-per-month" disabled="disabled"></label>
<label>Per year: <input type="number" id="result-per-year" disabled="disabled"></label>
</div>
<script>
const { Binding, $$, reactive, claritive } = vjs_reactivity;
const income = claritive({
amount: new Binding("#amount"),
basis: new Binding("#income-basis")
});
const expenses = claritive({
groceries: new Binding("#groceries"),
bills: new Binding("#bills"),
entertainment: new Binding("#entertainment"),
basis: new Binding("#expenses-basis")
});
const result = claritive({
perDay: new Binding("#result-per-day"),
perWeek: new Binding("#result-per-week"),
perMonth: new Binding("#result-per-month"),
perYear: new Binding("#result-per-year")
});
$$(income.$amount, income.$basis, expenses.$groceries, expenses.$bills, expenses.$entertainment, expenses.$basis,
(inc, incomeBasis, groc, bills, ent, expensesBasis) => {
let incomePerMonth, expensesPerMonth;
switch(incomeBasis) {
case "daily": {
incomePerMonth = inc * 30;
break;
};
case "weekly": {
incomePerMonth = inc * 4;
break;
};
case "bi-weekly": {
incomePerMonth = inc * 2;
break;
};
case "monthly": {
incomePerMonth = inc;
break;
};
case "yearly": {
incomePerMonth = inc / 12;
break;
}
default: throw new Error(`The basis you entered is incorrect.`);
}
switch (expensesBasis) {
case "daily": {
expensesPerMonth = (groc + bills + ent) * 30;
break;
};
case "weekly": {
expensesPerMonth = (groc + bills + ent) * 4;
break;
};
case "bi-weekly": {
expensesPerMonth = (groc + bills + ent) * 2;
break;
};
case "monthly": {
expensesPerMonth = (groc + bills + ent);
break;
};
case "yearly": {
expensesPerMonth = (groc + bills + ent) / 12;
break;
}
default: throw new Error(`The basis you entered is incorrect.`);
}
result.perDay = (incomePerMonth - expensesPerMonth) / 30;
result.perWeek = (incomePerMonth - expensesPerMonth) / 4;
result.perMonth = (incomePerMonth - expensesPerMonth);
result.perYear = (incomePerMonth - expensesPerMonth) * 12;
}
);
</script>