ATV By Example
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.

HTML

<div data-atv-controller="simple">
  <button data-atv-simple-action="click">
    Count 0
  </button>
</div>    

<script type="module">
  import { activate } from '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.

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. 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.allFactor.forEach((factor) => {
        if (factor.value) {
          product *= factor.value;
        } else {
          product *= factor.innerText;
        }
      });
      if (targets.product) {
        targets.product.innerText = product;
      }
      return product;
    }
  };
}

export { connect };
      
* * =
Other Controllers (aka Outlets)
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.

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.

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:

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.

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 };
      
Not yet connected.