#Astro actions + alpinejs

48 messages · Page 1 of 1 (latest)

fringe void
#

Почему ругается на Alpine.data...


<form id="contact-form"
x-data="contactForm"
@submit.prevent="submit"

<label>
Имя
<input type="text" name="name" x-model="name" @input="validate('name')" autocomplete="on" />
</label>
<p x-show="errors.name" x-text="errors.name" style="color:red;"></p>

<label>
Email
<input type="email" name="email" x-model="email" @input="validate('email')" autocomplete="on" />
</label>
<p x-show="errors.email" x-text="errors.email" style="color:red;"></p>

<label>
Телефон
<input type="tel" name="phone" x-model="phone" @input="validate('phone')" autocomplete="on" />
</label>
<p x-show="errors.phone" x-text="errors.phone" style="color:red;"></p>

<input type="text" name="username" style="display:none" tabindex="-1" autocomplete="off" />

<button type="submit" :disabled="!valid">Отправить</button>

<p x-show="success" style="color:green;">✅ Заявка отправлена!</p>
<p x-show="serverError" x-text="serverError" style="color:red;"></p>
</form>

<script>
document.addEventListener('alpine:init', () => {
Alpine.data('contactForm', () => ({
name: '',
email: '',
phone: '',
errors: {},
valid: false,
success: false,
serverError: '',

  rules: {

...
},

  validate(field) {
    const msg = this.rules[field](this[field]);
    if (msg) {
      this.errors[field] = msg;
    } else {
      delete this.errors[field];
    }
    this.valid = Object.keys(this.errors).length === 0;
  },

  async submit() {

....
},
}));
});
</script>

proven girderBOT
#

Hi @fringe void. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Проблема с Alpine.data в Astro

Скорее всего, ошибка возникает потому, что ваш <script> выполняется до того, как Alpine.js успевает инициализироваться на странице, либо Alpine недоступен как глобальная переменная в момент выполнения скрипта.

При использовании интеграции @astrojs/alpinejs скрипт Alpine добавляется автоматически на каждую страницу. [Alpine integration] Однако важно правильно слушать событие alpine:init.

Решение

Убедитесь, что ваш <script> использует событие alpine:init и что он объявлен корректно в .astro-файле. В Astro скрипты в <script>-тегах по умолчанию бандлируются, и Alpine может быть недоступен как глобальная переменная.

Попробуйте явно импортировать Alpine и регистрировать компонент через него:

<script>
  import Alpine from 'alpinejs';

  document.addEventListener('alpine:init', () => {
    Alpine.data('contactForm', () => ({
      name: '',
      email: '',
      phone: '',
      errors: {},
      valid: false,
      success: false,
      serverError: '',

      validate(field) {
        // ваша логика
      },

      async submit() {
        // ваша логика
      },
    }));
  });
</script>

Почему это работает

Интеграция @astrojs/alpinejs устанавливает alpinejs как зависимость, и вы можете импортировать его напрямую. [Alpine integration] Это гарантирует, что Alpine доступен в момент регистрации компонента через Alpine.data(...).

Также убедитесь, что в astro.config.mjs интеграция подключена:

import { defineConfig } from 'astro/config';
import alpinejs from '@astrojs/alpinejs';

export default defineConfig({
  integrations: [alpinejs()],
});
#

[Alpine integration]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder а тут что не так

async submit() {
this.success = false;
this.serverError = '';

    const form = document.getElementById('contact-form') as HTMLFormElement;
    const formData = new FormData(form);
    const { data, error } = await actions.form.submitForm(formData);

    if (error) {
      if (error.fields) {
        if (error.fields.name) this.errors.name = error.fields.name.join(', ');
        if (error.fields.email) this.errors.email = error.fields.email.join(', ');
        if (error.fields.phone) this.errors.phone = error.fields.phone.join(', ');
      } else {
        this.serverError = error.message || 'Ошибка';
      }
      this.valid = false;
      return;
    }

    if (data?.success) {
      this.success = true;
      this.name = '';
      this.email = '';
      this.phone = '';
      this.errors = {};
      form.reset();
    }
  },

Property 'fields' does not exist on type 'ActionError<{ name: string; email: string; phone: string; username?: string | null | undefined; }>'.

я добавил же // ts-ignore

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The error occurs because error.fields doesn't exist directly on ActionError. To check for input validation errors, you need to use the isInputError() utility from astro:actions — it narrows the type correctly and gives you access to the fields object. [Displaying form errors]

Using // @ts-ignore suppresses the error but doesn't fix the underlying type issue, and it's not the recommended approach.

Here's the corrected submit method:

import { actions, isInputError } from 'astro:actions';

async submit() {
  this.success = false;
  this.serverError = '';

  const form = document.getElementById('contact-form');
  const formData = new FormData(form);
  const { data, error } = await actions.form.submitForm(formData);

  if (error) {
    if (isInputError(error)) {
      // Now TypeScript knows `error.fields` exists
      if (error.fields.name) this.errors.name = error.fields.name.join(', ');
      if (error.fields.email) this.errors.email = error.fields.email.join(', ');
      if (error.fields.phone) this.errors.phone = error.fields.phone.join(', ');
    } else {
      this.serverError = error.message || 'Ошибка';
    }
    this.valid = false;
    return;
  }

  if (data?.success) {
    this.success = true;
    this.name = '';
    this.email = '';
    this.phone = '';
    this.errors = {};
    form.reset();
  }
},
#

The key change is wrapping the error.fields check with isInputError(error). This utility returns true only when the error is a validation error caused by invalid input, and TypeScript will correctly infer that error.fields exists inside that block. [isInputError reference]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder Property 'email' does not exist on type '{}'.

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

This TypeScript error comes from the errors object being typed as {} (an empty object), so TypeScript doesn't know it can have name, email, or phone keys.

The fix is to explicitly type the errors property in your Alpine component data. Since this is inside a <script> tag in Astro, you can define the type inline:

document.addEventListener('alpine:init', () => {
  Alpine.data('contactForm', () => ({
    name: '',
    email: '',
    phone: '',
    errors: {} as { name?: string; email?: string; phone?: string },
    valid: false,
    success: false,
    serverError: '',

    // ...rest of your component
  }));
});

By casting errors to { name?: string; email?: string; phone?: string }, TypeScript will know that errors.name, errors.email, and errors.phone are valid (optional) string properties, and the error will go away.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder
const formData = new FormData(form);

Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLFormElement | undefined'.
Type 'null' is not assignable to type 'HTMLFormElement | undefined'.

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The error is a TypeScript type issue: document.getElementById() returns HTMLElement | null, but FormData expects HTMLFormElement | undefined. You need to cast the result to HTMLFormElement after a null check.

Fix it like this:

const form = document.getElementById('contact-form') as HTMLFormElement | null;
if (!form) return;
const formData = new FormData(form);

Or more concisely with a non-null assertion (if you're certain the element exists):

const form = document.getElementById('contact-form') as HTMLFormElement;
const formData = new FormData(form);

The cast to HTMLFormElement tells TypeScript the element is specifically a form, and the null check (or ! assertion) satisfies the | null part of the type.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder
А тут че не так то

Alpine.data("contactForm", () => ({
name: "",
email: "",
phone: "",
errors: {} as { name?: string; email?: string; phone?: string },
valid: false,
success: false,
serverError: "",

  rules: {
    name: (v: string) => (v.length >= 2 ? "" : "Имя должно быть не короче 2 символов"),
    email: (v: string) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? "" : "Введите корректный email"),
    phone: (v: string) => (v.length >= 10 ? "" : "Телефон должен быть не короче 10 символов"),
  },

  validate(field) {
    const msg = this.rules[field](this[field]);
    if (msg) {
      this.errors[field] = msg;
    } else {
      delete this.errors[field];
    }
    this.valid = Object.keys(this.errors).length === 0;
  },
proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don't contain enough information to answer this specific TypeScript/Alpine.js type error in detail, as it's primarily a TypeScript typing issue rather than an Astro-specific one.

That said, the likely problem is that this.rules[field] and this[field] are not properly typed — TypeScript doesn't know that field is a key of rules or of the component data, so it complains about indexing.

You can fix it by typing the field parameter explicitly:

validate(field: 'name' | 'email' | 'phone') {
  const msg = this.rules[field](this[field] as string);
  if (msg) {
    this.errors[field] = msg;
  } else {
    delete this.errors[field];
  }
  this.valid = Object.keys(this.errors).length === 0;
},

If you could share the exact TypeScript error message you're seeing, I could give a more precise answer. However, note that this is a general TypeScript question — for Astro-specific issues (like actions, form handling, etc.), I'm happy to help further.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder Parameter 'field' implicitly has an 'any' type.

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

This is a continuation of the TypeScript typing issue from the previous message. The fix is exactly what was suggested — explicitly type the field parameter so TypeScript knows it's not any:

validate(field: 'name' | 'email' | 'phone') {
  const msg = this.rules[field](this[field] as string);
  if (msg) {
    this.errors[field] = msg;
  } else {
    delete this.errors[field];
  }
  this.valid = Object.keys(this.errors).length === 0;
},

The error Parameter 'field' implicitly has an 'any' type appears because TypeScript's strict mode (enabled by default in Astro's strict and strictest tsconfig templates) does not allow implicit any types. [TSConfig templates] By explicitly typing field as 'name' | 'email' | 'phone', you tell TypeScript exactly what values it can be, resolving both this error and the indexing errors on this.rules[field] and this.errors[field].

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder
а можно ли правила валидации взять из zod-схемы, вместо того чтоб ручками писать

rules: {
name: (v: string) => (v.length >= 2 ? "" : "Имя должно быть не короче 2 символов"),
email: (v: string) => (/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(v) ? "" : "Введите корректный email"),
phone: (v: string) => (v.length >= 10 ? "" : "Телефон должен быть не короче 10 символов"),
},

?

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Yes, you can derive client-side validation from your Zod schema. Since your action already has a Zod input schema, you can reuse it on the client side by importing z from astro/zod and calling .safeParse() on individual fields.

Here's the approach — define your schema once in your actions file:

// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';

const contactSchema = z.object({
  name: z.string().min(2, { error: "Имя должно быть не короче 2 символов" }),
  email: z.email({ error: "Введите корректный email" }),
  phone: z.string().min(10, { error: "Телефон должен быть не короче 10 символов" }),
});

export const server = {
  form: {
    submitForm: defineAction({
      accept: 'form',
      input: contactSchema,
      handler: async (input) => {
        // ...
        return { success: true };
      },
    }),
  },
};

Then in your Alpine component script, import the schema and use .safeParse() to validate individual fields: [Zod API Reference]

import { z } from 'astro/zod';

const contactSchema = z.object({
  name: z.string().min(2, { error: "Имя должно быть не короче 2 символов" }),
  email: z.email({ error: "Введите корректный email" }),
  phone: z.string().min(10, { error: "Телефон должен быть не короче 10 символов" }),
});

document.addEventListener('alpine:init', () => {
  Alpine.data('contactForm', () => ({
    name: '',
    email: '',
    phone: '',
    errors: {} as { name?: string; email?: string; phone?: string },
    valid: false,
    success: false,
    serverError: '',
#

validate(field: 'name' | 'email' | 'phone') {
const result = contactSchema.shape[field].safeParse(this[field]);
if (!result.success) {
this.errors[field] = result.error.issues[0]?.message ?? 'Ошибка';
} else {
delete this.errors[field];
}
this.valid = Object.keys(this.errors).length === 0;
},

// ...submit, etc.

}));
});


The key points:
- `contactSchema.shape[field]` gives you the individual Zod validator for that field.
- `.safeParse()` returns `{ success, error }` without throwing.
- `result.error.issues[0]?.message` extracts the first error message.

Note the Zod 4 syntax for error messages uses `{ error: "..." }` instead of `{ message: "..." }`, and `z.email()` is now a top-level method instead of `z.string().email()`. [[Zod 4 upgrade](<https://docs.astro.build/en/guides/upgrade-to/v6/#zod-4>)]
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder чтото я не вижу импорта схемы. и почему ты забыл русский язык

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Извините за путаницу! Вы правы — в предыдущем ответе я показал схему дважды (в actions/index.ts и в скрипте), но не показал, как импортировать её из одного места в другое.

Лучший подход — вынести схему в отдельный файл и импортировать её и в actions, и в Alpine-компонент:

// src/schemas/contact.ts
import { z } from 'astro/zod';

export const contactSchema = z.object({
  name: z.string().min(2, { error: "Имя должно быть не короче 2 символов" }),
  email: z.email({ error: "Введите корректный email" }),
  phone: z.string().min(10, { error: "Телефон должен быть не короче 10 символов" }),
});
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { contactSchema } from '../schemas/contact';

export const server = {
  form: {
    submitForm: defineAction({
      accept: 'form',
      input: contactSchema,
      handler: async (input) => {
        // ...
        return { success: true };
      },
    }),
  },
};
// В вашем <script> в .astro-файле
import { contactSchema } from '../schemas/contact';
import { actions, isInputError } from 'astro:actions';

document.addEventListener('alpine:init', () => {
  Alpine.data('contactForm', () => ({
    name: '',
    email: '',
    phone: '',
    errors: {} as { name?: string; email?: string; phone?: string },
    valid: false,
    success: false,
    serverError: '',

    validate(field: 'name' | 'email' | 'phone') {
      const result = contactSchema.shape[field].safeParse(this[field]);
      if (!result.success) {
        this.errors[field] = result.error.issues[0]?.message ?? 'Ошибка';
      } else {
        delete this.errors[field];
      }
      this.valid = Object.keys(this.errors).length === 0;
    },

    async submit() {
      // ...
    },
  }));
});
#

Таким образом схема определяется один раз и используется и для серверной валидации в actions [Actions guide], и для клиентской валидации в Alpine. [Zod API Reference]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder
Property '$el' does not exist on type '{ name: string; email: string; phone: string; errors: { name?: string | undefined; email?: string | undefined; phone?: string | undefined; }; valid: boolean; success: boolean; serverError: string; validate(field: "name" | ... 1 more ... | "phone"): void; submit(): Promise<...>; }'.

я использую конструкцию
const formData = new FormData(this.$el);
<form id="contact-form" x-data="contactForm" @submit.prevent="submit">

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Эта ошибка — чисто TypeScript-проблема: Alpine's $el и другие магические свойства ($refs, $store и т.д.) не типизированы в объекте, который вы передаёте в Alpine.data().

К сожалению, знания из предоставленных источников не содержат информации о типизации магических свойств Alpine.js ($el, $refs и т.д.) в TypeScript.

Однако практическое решение — просто получить форму через document.getElementById вместо this.$el, что вы уже делали ранее:

async submit() {
  this.success = false;
  this.serverError = '';

  const form = document.getElementById('contact-form') as HTMLFormElement;
  if (!form) return;
  const formData = new FormData(form);

  // ...остальная логика
},

Либо, если хотите сохранить this.$el, можно обойти TypeScript через явное приведение типа:

async submit() {
  const form = (this as any).$el as HTMLFormElement;
  const formData = new FormData(form);

  // ...
},

Первый вариант (через getElementById) предпочтительнее, так как он типобезопасен и не требует any.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder чет я не понял. без JS астро экшены не работают?

proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

На самом деле, Astro Actions могут работать без клиентского JavaScript — через нативные HTML-формы с атрибутом action.

Для этого нужно:

  1. Добавить method="POST" и action={actions.yourAction} к форме.
  2. Страница должна быть on-demand rendered (не статической).
---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.form.submitForm);
if (result && !result.error) {
  return Astro.redirect('/success');
}
---

<form method="POST" action={actions.form.submitForm}>
  <input type="text" name="name" />
  <input type="email" name="email" />
  <button type="submit">Отправить</button>
</form>

Astro сам обработает POST-запрос на сервере, а результат можно получить через Astro.getActionResult(). [HTML form action]


Однако в вашем случае с Alpine.js вы используете @submit.prevent="submit" и вызываете action через JavaScript (actions.form.submitForm(formData)). Это клиентский подход, и он требует JS. [Validating form data]

Если хотите zero-JS форму — используйте нативный action={...} на форме. Если хотите Alpine с валидацией — JS обязателен.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@fringe void Kapa.ai is still learning and improving, please let me know how I did by reacting below.

fringe void
#

@proven girder что не так то


export const prerender = false;

const cart = (await Astro.session?.get("cart")) || [];
const total = cart.reduce((sum: number, item: any) => sum + item.price * item.qty, 0);

<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Корзина</title>
</head>
<body>
<h1>Корзина</h1>

{
  cart.length === 0 ? (
    <p>Корзина пуста</p>
  ) : (
    <>
      <ul>
        {cart.map((item: any) => (
          <li>
            {item.name} — {item.qty} шт. × {item.price} ₽ = {item.qty * item.price} ₽
          </li>
        ))}
      </ul>
      <p>
        <strong>Итого: {total} ₽</strong>
      </p>

      <button id="pay-btn">Оплатить {total} ₽</button>
      <div id="payment-form" />
    </>
  )
}
<a href="/">← В магазин</a>

<div id="cart-data" type="application/json">{JSON.stringify(cart)}</div>
<div id="cart-total" style="display:none">{total}</div>

</body>
</html>

<script is:inline src="https://yookassa.ru/checkout-widget/v1/checkout-widget.js"></script>
<script>
import { actions } from 'astro:actions';

  const cartData = {JSON.stringify(cart)};
  const total = {total};

  document.getElementById('pay-btn')?.addEventListener('click', async () => {
    const { data, error } = await actions.order.create({
      amount: total,
      email: 'test@test.ru',
      cart: cartData,
    });

    if (error) {
      alert('Ошибка: ' + error.message);
      return;
    }

    if (data?.confirmationToken) {
      new window.YooMoneyCheckoutWidget({
        confirmation_token: data.confirmationToken,
        return_url: `http://localhost:4321/success/${data.documentId}`,
        error_callback: (err) => console.error(err),
      }).render('payment-form');
    }
  });
</script>
proven girderBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Вижу несколько проблем:

1. is:inline блокирует импорты

У вас внешний скрипт с is:inline, а следующий <script> пытается использовать import { actions } from 'astro:actions'. Но is:inline относится только к первому тегу — второй скрипт без атрибутов обрабатывается Astro как модуль, и это нормально.

Однако проблема в том, что оба <script> стоят после </html> — это невалидный HTML. Перенесите их внутрь <body> или <head>.

2. Нельзя вставлять серверные переменные напрямую в <script>

Это неправильно:

const cartData = {JSON.stringify(cart)};
const total = {total};

Astro не интерполирует переменные из frontmatter в <script>-теги. [Script processing]

Вы уже правильно сделали — вынесли данные в DOM-элементы:

<div id="cart-data" type="application/json">{JSON.stringify(cart)}</div>
<div id="cart-total" style="display:none">{total}</div>

Читайте их из DOM в скрипте:

import { actions } from 'astro:actions';

const cartData = JSON.parse(document.getElementById('cart-data')?.textContent || '[]');
const total = Number(document.getElementById('cart-total')?.textContent || '0');

document.getElementById('pay-btn')?.addEventListener('click', async () => {
  const { data, error } = await actions.order.create({
    amount: total,
    email: 'test@test.ru',
    cart: cartData,
  });

  if (error) {
    alert('Ошибка: ' + error.message);
    return;
  }

  if (data?.confirmationToken) {
    new window.YooMoneyCheckoutWidget({
      confirmation_token: data.confirmationToken,
      return_url: `<http://localhost:4321/success/${data.documentId}`,>
      error_callback: (err) => console.error(err),
    }).render('payment-form');
  }
});

3. Внешний скрипт с is:inline

#

Для внешнего скрипта из CDN is:inline обязателен — это правильно. [External scripts] Но убедитесь, что он загружается до вашего скрипта, который использует YooMoneyCheckoutWidget.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: