Возможно, вы уже слышали про Alpine.js. Если нет, то это "Vue.js на минималках". "Angular 1 для миллениалов". Называйте, как хотите, главное, чтобы вам было понятно.


Зачем нам еще один фреймворк? Ну, Alpine хорошо вписывается в свою нишу. По факту, он – альтернатива большим фреймворкам для сайтов, где эти большие фреймворки не нужны. Например, меня, модного web-developer'а, запрягли писать многостраничный сайт. Мне нужно элементарно сделать форму и всякий другой интерактив. Что я буду делать? Возьму jQuery – друзья не поймут, на чистом JS всё писать тоже не комильфо. К тому же я уже знаю реакты, ангуляры и вью, знаю, что такое data-driven подход. Как мне теперь данные до отправки в HTML хранить?


Тут и приходит на помощь Alpine.js. Можно, конечно, Vue или React прикрутить. Но React без JSX никто в здравном уме писать не будет, а Vue минифицированный через CDN весит 34 kB (против 8.1 kB у Alpine). Так и получается, что выбор падает на Alpine.


Недавно тут на Хабре уже были ознакомительные статьи про Alpine.js. Если не считать того, что сайт вырезал везде упоминания про тег template, статьи получились неплохие.


Я, чтобы не повторяться, не буду пересказывать документацию, благо она очень короткая (весь фреймворк – это 13 директив и 6 магических свойств) и скоро уже будет доступна на русском языке (на момент написания статьи перевод находится на одобрении у создателя, но его уже можно прочитать в моем форке).


UPD: Документация на русском уже доступна.

Если вам интересно и/или вы не знакомы с Vue, настоятельно рекомендую. Для тех, кто с Vue знаком, краткое описание ключевых отличий:


  • Везде вместо v- используем x-, т.е. не v-model, а x-model; не v-bind, а x-bind Почему? Это ваше задание на дом :)
  • x-if и x-for могут использоваться только в теге template. Издержки отсутствия Virtual DOM.
  • Всеми любимой интерполяции {{}} нет. Пишите x-text и x-html, как настоящие мужики уже давно делают во Vue.

Об остальном в процессе.


Не буду придумывать ничего оригинального, фантазия слабая. Давайте сделаем всем ненавистный тудушник. Почему не что-нибудь другое? Первое оправдание я уже назвал – плохая фантазия. Второе – все уже наизусть знают процесс создания тудушки, что позволит не отвлекатся на идею, а сконцентрироваться на реализации. Третье и мое любимое – "в туду есть все базовые функции, что позволяет нам посмотреть на все процессы" и т.д. и т.п.


Короче, воду лить я закончил, показываю код.


Начнем с голого HTML, куда через CDN вставим Alpine


<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>To-Do на Alpine.js</title>
    <script
      src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.3.5/dist/alpine.min.js"
      defer
    ></script>
  </head>
  <body></body>
</html>

Alpine самоинициализируется, ничего больше писать не надо. Он уже работает.


Итак, что дальше? Нам нужно создать компонент и задать ему первичные данные. Это делается с помощью директивы x-data.


<div x-data="{ name: 'Женя' }">
  <p>Привет, <span x-text="name"></span></p>
</div>

Этот <div> теперь считается отдельным компонентом, своей отдельной вселенной. Доступ к переменной name вне этого <div> получить честными способами нельзя.


Отлично, теперь давайте сделаем to-do. Начнем со списка:


<div
  x-data="{ todos: [{id: 1, title: 'купить хлеб', completed: false}, {id: 2, title: 'продать айфон', completed: false}, {id: 3, title: 'закончить этот курс', completed: false}, {id: 4, title: 'перестать быть банальным', completed: false}] }"
>
  <h1>Планы на сегодня:</h1>
  <ul>
    <template x-for="todo in todos" :key="todo.id">
      <li x-text="todo.title"></li>
    </template>
  </ul>
</div>

На клик назначим переключение выполнения туду. Для того, чтобы это работало, создадим CSS-класс .completed, который и будем назначать компоненту с помощью x-bind:class (принимает объект {[имя класса]: [условие]}). Еще добавим cursor: pointer на li, чтобы работать было приятнее.


<style>
  .completed {
    text-decoration: line-through;
  }

  li {
    cursor: pointer;
  }
</style>

<div
  x-data="{ todos: [{id: 1, title: 'купить хлеб', completed: false}, {id: 2, title: 'продать айфон', completed: false}, {id: 3, title: 'закончить этот курс', completed: false}, {id: 4, title: 'перестать быть банальным', completed: false}], toggleTodo: function(id) { var todo = this.todos.find((todo) => todo.id === id); todo.completed = !todo.completed; } }"
>
  <h1>Планы на сегодня:</h1>
  <ul>
    <template x-for="todo in todos" :key="todo.id">
      <li
        x-text="todo.title"
        @click="toggleTodo(todo.id)"
        :class="{'completed': todo.completed}"
      ></li>
    </template>
  </ul>
</div>

Если вам кажется, что это начинает выглядить отстойно и непонятно, то вам не кажется. Благо, ничего не мешает нам вывести все данные из x-data в функцию:


<div x-data="todos()">
  <h1>Планы на сегодня:</h1>
  <ul>
    <template x-for="todo in todos" :key="todo.id">
      <li
        x-text="todo.title"
        @click="toggleTodo(todo.id)"
        :class="{'completed': todo.completed}"
      ></li>
    </template>
  </ul>
</div>

<script>
  function todos() {
    return {
      todos: [
        { id: 1, title: 'купить хлеб', completed: false },
        { id: 2, title: 'продать айфон', completed: false },
        { id: 3, title: 'закончить этот курс', completed: false },
        { id: 4, title: 'перестать быть банальным', completed: false },
      ],
      toggleTodo: function (id) {
        var todo = this.todos.find((todo) => todo.id === id);
        todo.completed = !todo.completed;
      },
    };
  }
</script>

Вот теперь красиво… и еще больше похоже на Vue.


Добавим input для новых задач:


<div x-data="todos()">
  <!-- ... -->
  <div>
    <h4>Добавить новую задачу:</h4>
    <input type="text" x-model="inputValue" />
    <button @click="addTodo()">Добавить</button>
  </div>
</div>

<script>
  function todos() {
    return {
      // ...
      inputValue: '',
      addTodo: function () {
        if (!this.inputValue) {
          return;
        }

        this.todos.push({
          id: Date.now(),
          title: this.inputValue,
          completed: false,
        });
        this.inputValue = '';
      },
    };
  }
</script>

Метода push() достаточно, чтобы Alpine понял, что что-то надо менять.


Последний штрих – кнопка удаления туду. В <li> создадим два span: в одном будет текст, во втором – крестик.


<ul>
  <template x-for="todo in todos" :key="todo.id">
    <li @click="toggleTodo(todo.id)" :class="{'completed': todo.completed}">
      <span x-text="todo.title"></span>
      <span @click="deleteTodo(todo.id)">&times;</span>
    </li>
  </template>
</ul>

<script>
  function todos() {
    return {
      // ...
      deleteTodo: function (id) {
        this.todos = this.todos.filter((todo) => todo.id !== id);
      },
    };
  }
</script>

Будем считать тудушник готовым.


Но для пущего удовлетворения, перед тем как закончить, добавим немного асинхронного кода, забирая наши задачи из REST API. Конечно же, апишкой нам послужит многострадальный JSON Placeholder. А для загрузки данных мы будем использовать директиву x-init.


<div x-data="todos()" x-init="fetchTodos()">
  <!-- ... -->
</div>

<script>
  function todos() {
    return {
      // ...
      todos: [],
      fetchTodos: function () {
        fetch('https://jsonplaceholder.typicode.com/todos')
          .then((response) => response.json())
          .then((data) => {
            this.todos = data.slice(0, 10);
          });
      },
    };
  }
</script>

x-init выполняет выражение при инициализации DOM (аналог created во Vue). Если передать колбэк – то, сразу после инициализации (аналог mounted).
И да, я знаю про ?_limit=10, но сейчас query не работали, так что обходимся тем, что есть.


На этом закончим. Мы охватили базовые функции Alpine.js. Остальное можно прочитать в документации. В принципе, всё можно было прочитать в документации. Но, надеюсь, этот пример поможет вам легче в ней ориентироваться.


Весь код урока я загрузил в этот sandbox. Добавил немного стилей, чтобы глаза не резало, остальное всё то же самое.


P.S. Для англоговорящих еще хочу посоветовать "почти полностью бесплатный" скринкаст от создателя Alpine.js. Он так хорош, что даже ссылкой жалко делиться. Ну ладно, вот. В нем автор рассказывает о том, как создать такой фреймворк, как Alpine, и что это совсем не так сложно, как может казаться.