Alpine.js на конкретном примере

Возможно, вы уже слышали про 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, и что это совсем не так сложно, как может казаться.

Читаем продолжение тут: Alpine.js – события и глобальное хранилище данных.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    +2
    Так и получается, что выбор падает на Alpine

    Preact 3kb gzipped и знакомый многим React синтаксис, хочешь с JSX, хочешь с Hyperscript, хочешь так.
    От Alpine впечатление осталось что он как раз выполняет ту же функцию для Vue что и Preact для React — максимально облегчённый фреймворк с привычным синтаксисом для простых применений вполне имеющий право на жизнь и свою маленькую нишу
      0
      Возьму jQuery – друзья не поймут

      поясните плз для тех кто не в теме
        0

        У модных разработчиков jQuery не в почёте. Он как deprecated.

          0

          Наверное, имеется ввиду то, что сейчас чистый JS достаточно удобный, чтобы отказаться от jQuery.

            0
            jQuery – это прекрасная библиотека, которую должен знать любой уважающий себя фронтенд-разработчик. Он был must-have во времена, когда никто не говорил о совместимости браузеров, не перекрестившись.
            Но сейчас во фронтенде балом правит Flux-архитектура – данные во главе угла, а внешний вид/интерфейс – лишь их отображение. Пользователь делает действие, данные меняются, вслед меняется отображение. Так сейчас работают все современные фреймворки/библиотеки.
            Насколько я знаю, jQuery в эту парадигму вписывается не очень. Он все еще бешено популярен, в первую очередь из-за тонны плагинов (Owl Carousel и т.п.), но использовать его как основную библиотеку для построения внешнего вида – архаизм.
            И да, как уже сказал Coral_Reef, не менее важно, что самые крутые штуки из jQuery уже были перенесены в ванильный JS (querySelector, например).
              0
              должен знать любой уважающий себя фронтенд-разработчик

              не должен.

            –2
            Любой легковесный фреймворк рано или поздно разжирееет
              +1
              Спасибо, что избавил от необходимости создания третьей статьи по теме)))
                0
                Спасибо за отличные введения для моей статьи)
                  0
                  Мне показалось что это был сарказм. И человек немого не доволен.
                    +1
                    Никакого сарказма, я на самом деле рад, что вдохновил кого-то на статью
                0
                Все хорошо, но есть один нюанс: при удалении todo, выбрасывается исключение Uncaught TypeError: Cannot read property 'completed' of undefined. Решить проблему можно добавлением if (todo !== undefined) перед todo.completed = !todo.completed. fetchTodos я бы реализовал так:
                async function () {
                    const response = await fetch("https://jsonplaceholder.typicode.com/todos")
                    const data = await response.json()
                    this.todos = data.slice(0, 10)
                }

                Спасибо за статью.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое