It seems to me like the x-on directive does not work without the presence of an x-data directive on a parent element.
For example, the below does not work
<button
x-on:click='console.log("hello")'>
left
</button>
It needs to be converted as following for it to work
<div x-data="{}">
<button
x-on:click='console.log("hello")'>
left
</button>
</div>
If I am understanding it correctly and an x-data directive is necessary, why would it be that way?
The x-data directive is required because of the main philosophy of Alpine.js framework.
With a full SPA (single page application) framework, like Vue.js, the main index.html file usually has only a single empty element inside <body>, like <div id="app"></div>. Then we mount our Vue.js application to the #main element and Vue.js takes over the entire frontend. After that by default everything is assumed to be a Vue.js component, like <MyComponent :some-data="42"></MyComponent>, so we don't have to mark them in any other way.
However, Alpine.js follows a different path. It is not intended to be a full SPA framework, like Vue.js. Its main purpose is to "enhance" frontend markup code generated by a backend framework like Django/Laravel/etc (the author of Alpine.js, Caleb Porzio, is the author of Laravel Livewire). So here by default a <button> element is assumed to be a normal button, not an Alpine.js component, therefore Alpine.js does not scan it for Alpine.js specific attributes, like x-on. We have to add the x-data attribute to an HTML element like <button> to mark it as an Alpine.js component. After that Alpine.js scans for directives as such, and takes over the handling of the marked element. This method works well with backend frameworks, where we usually only want to make some elements interactive on the frontend with Alpine.js, but any other logic is handled by the backend.
Nevertheless, if you want you can add an empty x-data attribute to the main <div> element in your markup, that makes Alpine.js work as an SPA framework, so it will scan every element for Alpine.js specific attributes. That's totally fine if you want to use Alpine.js this way. But usually we can define smaller, reusable components, so we put the x-data={} attribute to the specific element or to closest ancestor of the element.