A is for Action
The most basic thing ATV can do is attach a listener (action) to an element in the DOM.
In this case, we wait for a button click and then change the button text with an incrementing count.
The callback recieves the element of the button as its first argument.
Please note, this example shows a global counter. See "Nesting" section for how to isolate your UI state.
The callback recieves the element of the button as its first argument.
Please note, this example shows a global counter. See "Nesting" section for how to isolate your UI state.
HTML
<div data-atv-controller="simple"> <button data-atv-simple-action="click"> Count 0 </button> </div> <script type="module"> import { activate } from '@sbrew.com/atv'; activate(); </script>
JavaScript
// app/javascripts/controllers/simple_atv.js function connect() { let counter = 0; return { click: function(actor) { counter += 1; actor.innerText = `Count ${counter}`; } }; } export { connect };
T is for Target
An ATV action can result in a change to a target element within the scope of the controller.
Here is the classic example to show a button affecting a target:
It lets you type a greeting, and when you press the button, the “hello” controller responds
by placing your input into a greeting div on the page.
The receiving `div` is the target.
The receiving `div` is the target.
HTML
<div data-atv-controller="hello"> <input data-atv-hello-target="name" type="text"> <button data-atv-hello-action="click->greet"> Greet </button> <span data-atv-hello-target="output"> </span> </div>
JavaScript
// app/javascripts/controllers/hello_atv.js function connect(targets) { return { greet: function() { targets.output.textContent = `Hello, ${targets.name.value}!`; } }; } export { connect };
V is for Value
Sometimes you might want to provide parameters or data to the controller
at rendering time.
In this case, you can pass in values (a JSON encoded hash) and use them inside the action callbacks as follows:
HTML
<div data-atv-controller="config" data-atv-config-values='{"answer":"My favorite color is blue!"}'> <button data-atv-config-action="click->reveal"> Reveal the mystery answer </button> <span data-atv-config-target="output"> </span> </div>
JavaScript
// config_atv.js function connect(targets, values) { return { reveal: function() { targets.output.textContent = values.answer; } }; } export { connect };
Nesting
ATVs can be nested! However, thanks to ES6 modules being global, you must store any state in a closure.
If a function is returned instead of a regular object of callbacks,
the provided function must return the same structure as the non-nesting (global) version above.
HTML
<div data-atv-controller="nesting"> <button data-atv-nesting-action="click"> Count 0 </button> <div data-atv-controller="nesting"> <button data-atv-nesting-action="click"> Count 0 </button> </div> </div>
JavaScript
// app/javascripts/controllers/nesting_atv.js function connect() { return function() { let counter = 0; return { click: function(actor) { counter += 1; actor.innerText = `Count ${counter}`; } }; }; } export { connect };
Multiple Actions
Your DOM elements can have more than one action.
HTML
<div data-atv-controller="multiple"> <button data-atv-multiple-actions='["click", "click->clack"]' style="background-color: green;"> Count 0 </button> </div>
JavaScript
// app/javascripts/controllers/multiple_atv.js function connect() { let counter = 0; return { click: function(actor) { counter += 1; actor.innerText = `Count ${counter}`; }, clack: function(actor) { actor.styles.backgroundColor = "bluegreen".replace(actor.styles.backgroundColor, ""); } }; } export { connect };
Multiple Targets and checking for Targets
Your DOM elements can have more than one target with the same name.
The complete list is available in the targets object in two forms; with an “s” suffix and with an “all” prefix.
If your targets are called “myThing”, you can find them in “targets.myThings” and also “targets.allMyThing”.
You can also ask the targets object if a target exists.
HTML
<div data-atv-controller="multiplier"> <input type="number" value="3" data-atv-multiplier-target="factor"></input> * <input type="number" value="4" data-atv-multiplier-target="factor"></input> * <input type="number" value="5" data-atv-multiplier-target="factor"></input> = <span data-atv-multiplier-target="product"></span> <button data-atv-multiplier-action="click->multiply"> Calculate </button> </div>
JavaScript
// app/javascripts/controllers/multiplier_atv.js function connect(targets) { return { multiply: function() { let product = 1; targets.factors.forEach((factor) => { if (factor.value) { product *= factor.value; } else { product *= factor.innerText; } }); if (targets.product) { targets.product.innerText = product; } return product; } }; } export { connect };
*
*
=
Outside the Controllers (aka Window events, Outlets)
Generally speaking, interaction between ATV controllers and with other parts of the
DOM ecosystem
can (and probably should)
be done using a lightweight event library such as
nanoevents.
However, ATV controllers can talk to other controllers using CSS selector patterns. We can use the same product controller from the previous example as follows:
However, ATV controllers can talk to other controllers using CSS selector patterns. We can use the same product controller from the previous example as follows:
HTML
<div class="product" data-atv-controller="multiplier"> <input type="number" value="1" data-atv-multiplier-target="factor"></input> * <input type="number" value="2" data-atv-multiplier-target="factor"></input> </div> + <div class="product" data-atv-controller="multiplier"> <input type="number" value="3" data-atv-multiplier-target="factor"></input> * <input type="number" value="4" data-atv-multiplier-target="factor"></input> </div> = <div data-atv-controller="adder"> <span data-atv-adder-target="sum"></span> <button data-atv-adder-action="click->add"> Calculate </button> </div>
JavaScript
// app/javascripts/controllers/adder_atv.js function connect(targets, _values, _root, controllers) { return { add: function() { let sum = 0; controllers(".product", "multiplier", (controller) => { sum += controller.actions.multiply(); }); targets.sum.innerText = sum; } }; } export { connect };
*
+
*
=
Action: Events
ATV actions receive the element they are attached to (actor) and the event itself as parameters.
You can use the data in this event to distinguish between keyboard input, for example.
Here we prevent the user from typing anything other than letters for a first name.
HTML
<div data-atv-controller="events"> <label for='name'>First name:<label> <input id='name' data-atv-events-action="keydown"></input> </div>
JavaScript
// app/javascript/controllers/events_atv.js function connect() { return { keydown: function(_actor, event) { const key = event.key; if (key.length === 1 && !/[a-zA-Z]/.test(key)) { event.preventDefault(); } } }; } export { connect };
Action: Parameters
Actions can take simple string parameters. This is useful if you need to specify parameters dynamically on the ERB,
or if you have multiple actions hitting the same target but wanting to do something slightly different.
Note: If you have mutiple actions and more than one parameter (i.e. a comma separated list) to a given action, you must specify the actions in a JSON encoded array so ATV doesn't get confused about what the commas mean.
The following example picks a new random number every time the controller loads and uses it to increment the counter.
Note: If you have mutiple actions and more than one parameter (i.e. a comma separated list) to a given action, you must specify the actions in a JSON encoded array so ATV doesn't get confused about what the commas mean.
The following example picks a new random number every time the controller loads and uses it to increment the counter.
HTML
<div data-atv-controller="parameters"> <button data-atv-parameters-action="click(Count, <%= rand(2..10) %>)"> Start </button> </div>
JavaScript
// app/javascripts/controllers/parameters_atv.js function connect() { let counter = 0; return { click: function(actor, _event, params) { const [label, increment] = params; counter = counter + Number(increment); actor.innerText = `${label} ${counter}`; } }; } export { connect };
Action: Sequences
Regular ATV actions are fired off independently as generated by the DOM.
For more complex interactions, we can combine multiple ATV controllers on a single DOM element
and actions in sequences.
If the action returns falsy, the sequence is terminated. Try entering zero as the dividend below — the multiplication is not attempted.
If the action returns falsy, the sequence is terminated. Try entering zero as the dividend below — the multiplication is not attempted.
HTML
<div data-atv-controller="divider, multiplier"> (<input type="number" value="12" data-atv-divider-target="dividend"></input> / <input type="number" value="3" data-atv-divider-target="divisor"></input>) = <span data-atv-divider-target="quotient" data-atv-multiplier-target="factor"></span> * <input type="number" value="6" data-atv-multiplier-target="factor"></input>) = <span data-atv-multiplier-target="product"></span> <button id="button" data-atv-actions="click->divider#divide, click->multiplier#multiply"> Calculate </button> </div>
JavaScript
// app/javascripts/controllers/divider_atv.js function connect(targets) { return { divide: function() { if (Number(targets.divisor.value) === 0) { targets.quotient.innerText = "You can't divide by zero!"; return false; } return targets.quotient.innerText = Number(targets.dividend.value) / Number(targets.divisor.value); } }; } export { connect };
( /
) =
*
) =
Connecting + Controller element
If a new controller appears in the DOM it will receive the connect method at that time.
Also note that connections receive a third parameter of the hosting element, as seen in the following code:
Also note that connections receive a third parameter of the hosting element, as seen in the following code:
HTML
<div data-atv-controller="connecting"> <button data-atv-connecting-action="click"> Create </button> <span data-atv-connecting-target="state"> Not yet connected. <span> </div>
JavaScript
// app/javascript/controllers/connecting_atv.js function connect(targets, _values, root) { targets.state.innerText = "Connected"; return { click: function() { root.insertAdjacentHTML("afterend", ` <div data-atv-controller="connecting"> <button data-atv-connecting-action="click"> Create </button> <span data-atv-connecting-target="state"> Not yet connected. </span> </div> `); } }; } export { connect };
Not yet connected.
Disconnecting
If you provide an action called disconnect, it will be called when the element hosting the ATV controller is removed from the DOM.
Also, since controllers interact with each other, ATV for the entire page is reloaded if another controller is removed. Before reloading, each controller will receive a disconnect call if it has one.
Not yet connected.
HTML
<div data-atv-controller="disconnecting"> <button data-atv-disconnecting-action="click"> Disconnect </button> </div> <span id="disconnected-state"> Not yet connected. <span>
JavaScript
// app/javascript/controllers/disconnecting_atv.js function connect(_targets, _values, root) { const connectedStateDiv = document.getElementById('disconnected-state'); connectedStateDiv.innerText = "Connected"; return { click: function() { root.parentNode.removeChild(root); }, disconnect: function() { connectedStateDiv.innerText = "Disconnected"; } }; } export { connect };
Namespacing
ATV plays well with others! If you give the activate function a prefix parameter, you can avoid namespace collisions in your HTML.
You can send ATV an empty string for no prefix.
Note that this example is exactly the same as the first one above, but lives in its own namespace apart from that one.
Note that this example is exactly the same as the first one above, but lives in its own namespace apart from that one.
HTML
<div data-my-atv-controller="simple"> <button data-my-atv-simple-action="click"> Count 0 </button> </div> <script type="module"> import { activate } from '@sbrew.com/atv'; activate("my-atv"); </script>
JavaScript
// app/javascripts/controllers/simple_atv.js function connect() { let counter = 0; return { click: function(actor) { counter += 1; actor.innerText = `Count ${counter}`; } }; } export { connect };