如何在 js 函数中更新 x-data 组件

How to update x-data component in a js function

Q1:

我正在尝试创建一个表单,其中填充了从 API 获取的初始值。每次用户编辑表单中的任何字段时,都应向 API 发送 POST 请求,然后更新初始值。我当前的解决方案有效,但是当我在 django 模板的 for 循环中使用它时,生成的 html 文件的可读性并不理想,因为脚本重复了很多次。我认为将提取脚本提取到函数中会使模板更具可读性,但我不知道如何更新包含用户在函数中以表单形式给出的所有值的 x-data 组件的内容。

当前的解决方案如下所示(样式等不必要的清理):

    <form action="/submitcars" method="POST" 
        x-data="{ dynamic_cars: [] }"
        x-init="dynamic_cars = await (await fetch('/dynamic_cars')).json()">
        {% csrf_token %}
        <div class="row-container">
            {% for row in rows %}
                {% for i in 0|range:3 %}
                  <div class="input_container car-{{i}}" x-show="open">
                    <div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited === 'true'">
                      <i class="material-icons refresh" @click="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited = 'false'">refresh</i>
                    </div>
                    <div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited === 'false'"></div>
                    <input required
                    step="any"
                    id=id_form_car_{{i}}-{{row.row_id}}
                    name=form_car_{{i}}-{{row.row_id}}
                    type="{{row.html_type}}"
                    tabindex="{{i}}" 
                    x-model.lazy="dynamic_cars.cars[{{i}}].{{row.row_id}}.value"
                    @change= "dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited = 'true',
                    dynamic_cars = await (
                    await fetch('/dynamic_cars', {
                      method: 'POST', 
                      headers: {
                        'Content-Type': 'application/json',
                        'X-CSRFToken': document.head.querySelector('meta[name=csrf-token]').content  
                    },
                    body: JSON.stringify(dynamic_cars)
                    })).json()"></input>
                    <div class="unit" x-text="dynamic_cars.cars[{{i}}].{{row.row_id}}.unit"></div>
                  </div>
                {% endfor %}
            {% endfor %}
        </div>
        <div class="submit-button">
          <button type="submit" value="submit">Save</button>
        </div>
    </form>
        ...

编辑:将 django 循环和变量添加到问题中。 row.row_id 具有 属性 名称,它告诉我在 CSS 网格的每一行上显示的信息(品牌、型号等)。所以这三辆车都具有这些属性。 row.row_id 匹配 dynamic_cars API 响应中的 属性 名称:

cars: [
        {
          {make: {value: "Toyota", unit: "", user_edited: "false"},
          {model: {value: "Camry", unit: "", user_edited: "false"},
          ...
          70+ more properties for each car
          ...
        },
        {...},
        {...}
      ]

而不是在 x-on:change 之后获取整个 POST-fetch,我想要这样的东西:

x-on:change="postCars(dynamic_cars)"

<script>
    async function postCars(dynamic_cars) {
      response = await fetch('/dynamic_cars', {
        method: 'POST', 
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken': document.head.querySelector('meta[name=csrf-token]').content  
      },
      body: JSON.stringify(dynamic_cars)
      })
      .then(response => {
          if(response.ok) return response.json();
             })
    }
</script>

通过这样做,我可以看到脚本中的 POST 请求在其负载中包含 dynamic_cars 中的数据,并且响应是正确的,但是 dynamic_cars 对象设置为空,如果输入被编辑,表单中的初始值将消失。这应该如何正确完成?

Q2:

我的另一个问题有点跑题,但可能与 javascript 的基础知识有关,是浏览器开发工具中的控制台显示错误消息:

Alpine Expression Error: Cannot read properties of undefined (reading 'value')

Expression: "dynamic_cars.cars[1].make.value"

AND

Uncaught TypeError: Cannot read properties of undefined (reading '1')

这是否意味着我必须在 x 数据中 define/initialize dynamic_cars 与 API returns 完全相同?如果 API 响应非常复杂并且包含大量数据甚至未知怎么办?打开 x-data="{}" 意味着数百行 javascript,我不会费心去编写和维护,因为当前的解决方案按预期工作,除了控制台错误。

您在控制台中看到这些错误消息的原因是您尝试将表单字段绑定到不存在的对象。在 Alpine.js 环境中,您只有一个空列表:x-data="{ dynamic_cars: [] }"。当 Alpine.js 最初尝试将输入字段绑定到相应的变量时,例如dynamic_cars.cars[1].make.valuedynamic_carscars属性都没有,实际上它连对象都不是,而是一个列表,所以也有类型错误。

在错误的数据绑定周期之后,Alpine.js 执行您在 x-init 中提供的获取后端并最终更新 dynamic_cars 变量的代码,所以现在它具有那些 attributes/fields 您之前尝试绑定表单域。令人惊讶的是它仍然以某种方式工作,但恕我直言,这是相当未定义的行为,应该避免。

但是由于您知道表单中 items/attributes 的数量,因此为它们创建正确的 JS 数据结构是微不足道的,因此 Alpine.js 可以将它们绑定到各自的表单字段。在我们获取后端之后,我们只需要用获取的数据更新这个对象,Alpine.js 自动更新 DOM。

<form action="/submitcars" method="POST" x-data="carsForm" @change="postCars">
  <div>
  {% for row in rows %}
      {% for i in 0|range:3 %}
        <div class="input_container car-{{i}}" x-show="open">
          <div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited">
            <i class="material-icons refresh" @click="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited = false">refresh</i>
          </div>
          <div class="edited_icon" x-show="dynamic_cars.cars[{{i}}].{{row.row_id}}.user_edited"></div>
          <input required
                step="any"
                id=id_form_car_{{i}}-{{row.row_id}}
                name=form_car_{{i}}-{{row.row_id}}
                type="{{row.html_type}}"
                tabindex="{{i}}"
                x-model.lazy="dynamic_cars.cars[{{i}}].{{row.row_id}}.value" />
          <div class="unit" x-text="dynamic_cars.cars[{{i}}].{{row.row_id}}.unit"></div>
        </div>
      {% endfor %}
  {% endfor %}
  </div>
  <div class="submit-button">
    <button type="submit" value="submit">Tallenna</button>
  </div>
</form>


<script>
document.addEventListener('alpine:init', () => {
  Alpine.data('carsForm', () => ({
    dynamic_cars: {cars: {
    {% for i in 0|range:3 %}
    {{ i }}: {
      {% for row in rows %}
      {{ row.row_id }}: {value: '', user_edited: true, unit: ''},
      {% endfor %}
    },
    {% endfor %}
    }},

    init() {
      this.getCars()
    },

    getCars() {
      // ... fetch backend as usual

      // Sync new data with local this.dynamic_cars
      this.syncData(response.json())
    },

    syncData(new_data) {
      for (let i in new_data.cars) {
        let car = new_data.cars[i]
        for (let property_name in car) {
          for (let prop_attr of ['value', 'unit', 'user_edited']) {
            this.dynamic_cars.cars[i][property_name][prop_attr] = car[property_name][prop_attr]
          }
        }
      }
    },

    postCars() {
      // Post to the backend with payload: JSON.stringify(this.dynamic_cars)

      // Sync response data with local this.dynamic_cars
      this.syncData(response.json())
    }
  }))
})
</script>

我们的 Alpine.js 组件称为 carsForm 并且我们在 alpine:init 事件中使用了 Alpine.data() 以确保 Alpine.js 在我们的环境中准备就绪。您会看到 HTML 模板仅包含最少的 Alpine.js 相关属性:x-data@change 以及数据绑定属性。

首先,我们创建一个空的 dynamic_cars Alpine.js 变量,它有尽可能多的行/items/attributes/等等,所以 Alpine.js 可以绑定它们中的每一个到相应的表单字段。

在从 init() 执行的 getCars() 方法中,我们获取后端并使用响应数据作为参数调用 syncData() 函数。此函数遍历深层嵌套的数据结构并更新本地 dynamic_cars 变量中的相应值。之后 Alpine.js 更新 DOM.

在用户更新表单字段后,@change 指令调用我们的 postCars() 方法 post 到后端,负载 dynamic_cars。当后端响应时,我们再次调用 syncData() 以使用来自后端的最新版本数据更新本地 dynamic_cars 变量。

注意:user_edited会被解析,所以不用做字符串比较,直接作为布尔变量使用即可。