javascriptclassmodulesingletonalpine.js

How to use JS classes (or similar concept) within AlpineJS modules


I use the following Todo module with AlpineJS in order to have a central logic/schema for data handling. It works in general. However, when I add multiple entries, they all point to the same instance; so when I add a new to do, all the todo's in the list adapt the new name.

To avoid this I tried using a class instead of just a module, however, this does not seem to work at all with AlpineJS.

Any idea how to best tackle this approach?

Notes Using Class Export:

  1. The error I get when trying to use a class is: Uncaught TypeError: Class constructor default cannot be invoked without 'new'
  2. I tried x-data="new todo()". Then I get the error: todo is not a constructor
  3. I also tried Alpine.data('todo', new Todo());. Then I get the error: Uncaught TypeError: callback.bind is not a function

Notes Using Function Export:

  1. It works with multiple (different) entries, when I just push item.name into the list, instead of (the whole) item, probably because it gets converted to a simple string, but I need the whole object unfortunatetly.
  2. When I tried to use todos.push(structuredClone(item)) I got the error: Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': # could not be cloned.

// <todo.js>
/* CLASS EXPORT: Does not work at all */
export default class {
    constructor(name, completed = false){
        this.item = {
          name: name,
          completed: completed
        }
    }
};

/* FUNCTION EXPORT: Works, but all items point to the same instance */
export default() => ({
    item: {
      name: '',
      completed: false
    }

});
<script type="module">
  import Alpine from './modules/alpine.js';
  import Todo from './modules/todo.js';

  Alpine.data('todo', Todo);
  Alpine.start();
</script>
 <article x-data="{ todos: [] }">

        <form x-data="todo" @submit.prevent="todos.push(item)">
            <input x-model="item.name"><button>Add</button>
        </form>

       <ul>
            <template x-for="todo in todos">
                <li x-text="todo.name"></li>
            </template>
       </ul>
    </article>


Solution

  • In todo.js you can first create the Todo class, then export it:

    class Todo {
        constructor(name, completed = false) {
            this.name = name
            this.completed = completed
        }
    }
    
    export default Todo
    

    Note that I removed item property from the Todo class.

    In the HTML file you also need to set Todo as a global class (window.Todo). You need to remove the Alpine.data('todo', Todo) and x-data="todo" parts since Todo is not an Alpine.js component, so don't try to use it as one. I also created a new property new_todo_name that holds the new todo's label until it is created with todos.push(new Todo(new_todo_name)).

    <script type="module">
      import Alpine from './modules/alpine.js'
      import Todo from './modules/todo.js'
      window.Todo = Todo
    
      Alpine.start()
    </script>
    <article x-data="{ todos: [], new_todo_name: '' }">
      <form @submit.prevent="todos.push(new Todo(new_todo_name)); new_todo_name = ''">
        <input x-model="new_todo_name"><button>Add</button>
      </form>
    
      <ul>
         <template x-for="todo in todos">
           <li x-text="todo.name"></li>
         </template>
      </ul>
    </article>