Day27. Stimulus 与非同步处理 - Ajax 的更优雅写法

我们在 Day21 提到 data-remote=trueDay25 提到了一些与Ajax 相关的例子,今天为正式的介绍在Rails如何完美的搭配Stimulus & Rails。今天我们会用3个情境教导读者如何使用Stimulus Ajax

dependent select lists

相依性的下拉式表单为标准以非同步处理的标准模型之一。以下的操作为先选品牌,当选品牌的同时,右方的下拉选单的选项会随着品牌的不同而有改变

⭐️ 下列为相依性表单的Slim & Stimulus Code

= card(controller: 'admin--blogs') do
  = card_header(title: 'Stimulus Ajax')
  = card_body do
    = tag.div class: 'mx-1 d-flex' do
      = datatable_select_tag Brand.all.pluck(:title, :id).prepend(['请选择品牌', nil]),
                             'brand-select', controller: 'admin--blogs',
                             target: 'brand', action: 'fetchStores', 
                             style: 'max-width: 300px'
      = datatable_select_tag [['请选择店舖', nil]], 'store-select',
                              controller: 'admin--blogs', target: 'store', 
                              style: 'max-width: 300px'
import { isEmpty, isNil, map, prepend, equals } from 'ramda';
import { Controller } from 'stimulus';
import Rails from '@rails/ujs';

/* 下拉式选单的选项 */
const selectOption = ({ value, text, select = null }) => `<option value=${value} ${equals(select, true) ? 'selected' : ''}>${text}</option>`

const fetchStores = ({ brandId, storeComp }) => {
  if(isEmpty(brandId)) {
    storeComp.innerHTML = selectOption({ value: '', text: '请选择店舖' })
    return
  }

  Rails.ajax({
    type: 'get',
    url: `/admin/brands/${brandId}/get_stores`,
    success: (response) => {
      /* 选单内容 */
      const resContent = prepend({ value: '', text: '请选择店舖'}, response)
      /* 找不到店柜的下拉式选单 => 跳出 */
      if (isNil(storeComp)) return
      /* Ajax 内容 */
      storeComp.innerHTML =
        `${map((e) => selectOption({ value: e.value,
            text: e.text }),
          resContent).join('')}`
    },
    error: (error) => {
      console.log('error response:', error);
    }
  })
}

export default class extends Controller {
  static targets = ['brand', 'store']

  // 依照厂牌取得店舖
  fetchStores() {
    fetchStores({ brandId: this.brandTarget.value,
      storeComp: this.storeTarget, prependWording: '请选择店舖'});
  }
}

可以看到打非同步的地方网址为 ⬇️

`/admin/brands/${brandId}/get_stores`

⭐️ 上面的网址列,对应的routes, controller 分别如下

resources :brands do 
  get :get_stores, on: :member
end
class Admin::BrandsController < Admin::ApplicationController
  # 下拉式选单的 Ajax
  def get_stores
    @stores = Brand.find_by_id(params[:id])&.stores

    render json: @stores&.map { |s| { value: s.id, text: s.title_zh } }
  end
end

⭐️ 透过非同步的动作取得的成功回应为底下的response,并且非同步回传的结果前方加上{ value: '', text: '请选择店舖'},并且使用selectOption组成下拉式选单的DOM,成为了搭配非同步处理的相依性选单。

import Rails from '@rails/ujs';

Rails.ajax({
  type: 'get',
  url: `/admin/brands/${brandId}/get_stores`,
  success: (response) => {
    /* 选单内容 */
    const resContent = prepend({ value: '', text: '请选择店舖'}, response)
    /* 找不到店柜的下拉式选单 => 跳出 */
    if (isNil(storeComp)) return
    /* Ajax 内容 */
    storeComp.innerHTML =
      `${map((e) => selectOption({ value: e.value,
                                  text: e.text }),
             resContent).join('')}`
  },
  error: (error) => {
    console.log('error response:', error);
  }
})

Ajax with click event

还记得Day26greet()吗? 当时我们使用greet() 来触发简单的JS动作,而我们可以透过事件触发非同步的动作,接着我们要来介绍,如何使用 Ajax 处理非同步的问题,以下为呈现的结果。

刚刚提到,只要打非同步就会需要打到後端,就需要事先设定并打通routes, controller,因此我们先将讲路径和逻辑写出来

⭐️ 下列为routes

resources :blogs do
  #====== ajax
  post :search, on: :collection
end

⭐️ 下列为 controller , view 。顺带一提,render partial的写法不只是view的专利,我们也可以在controller写。

module Admin
  class BlogsController < ApplicationController
    def search
      blog = Blog.find_by_id params[:id]
      
      render partial: 'searched_blog', locals: { blog: blog }
    end
  end
end

⭐️ 在 app/views/admin/blogs/_searched_blog 中写下欲渲染的画面

br
= log_template do
  = log_item title: 'id' do
    = blog&.id || ''
  = log_item title: '标题' do
    = blog&.title || '找不到标题'
  = log_item title: '内文' do
    = blog&.content || '找不到内文'

⭐️ 由於想要用实际的例子让读者感受使用Value的用法,我们将路径放在Value当中

= card(controller: 'admin--blogs', data: { 'admin--blogs-query-url-value': search_admin_blogs_path }) do
  = card_header(title: 'Stimulus Ajax')
  = card_body do
    // 内容省略......
    = tag.div class: 'mx-2 my-3 p-2', style: 'width: 300px; border: 1px solid black' do
      = tag.div(class: 'form-group string required admin_blogs_search_id')
        = tag.label tag.strong('搜寻id')
        = tag.input name: nil, class: "form-control string required",
                    data: { 'admin--blogs-target': 'ajaxId' }
      = button_tag '搜寻', type: 'button', data: { action: 'admin--blogs#ajaxGreet' },
                   class: 'btn btn-primary'
      = tag.div data: { 'admin--blogs-target': 'searchedContent' }
import { isEmpty, isNil, map, prepend, equals } from 'ramda';
import { Controller } from 'stimulus';
import Rails from '@rails/ujs';

/* 下拉式选单的选项 */
const selectOption = ({ value, text, select = null }) =>
  `<option value=${value} ${equals(select, true) ? 'selected' : ''}>${text}</option>`

const fetchStores = ({ brandId, storeComp }) => {
  if(isEmpty(brandId)) {
    storeComp.innerHTML = selectOption({ value: '', text: '请选择店舖' })
    return
  }

  Rails.ajax({
    type: 'get',
    url: `/admin/brands/${brandId}/get_stores`,
    success: (response) => {
      /* 选单内容 */
      const resContent = prepend({ value: '', text: '请选择店舖'}, response)
      /* 找不到店柜的下拉式选单 => 跳出 */
      if (isNil(storeComp)) return
      /* Ajax 内容 */
      storeComp.innerHTML =
        `${map((e) => selectOption({ value: e.value,
            text: e.text }),
          resContent).join('')}`
    },
    error: (error) => {
      console.log('error response:', error);
    }
  })
}

export default class extends Controller {
  static targets = ["searchedContent", 'ajaxId']
  static values = { queryUrl: String }

  connect() {
    console.log('this.queryUrlValue', this.queryUrlValue)
  }

  ajaxGreet() {
    Rails.ajax({
      type: 'post',
      url: this.queryUrlValue,
      data: new URLSearchParams({
        id: this.ajaxIdTarget.value
      }),
      success: (data, status, xhr) => {
        this.searchedContentTarget.innerHTML = xhr.response;
      },
      error: (error) => {
        console.log('error response:', error);
      }
    })
  }
}

我们将後端传过来的search_admin_blogs_path,传到 this.queryUrlValueRails.ajax,并且将参数 this.ajaxIdTarget.value 传进 Controller,并且将结果打回来显示在this.searchedContentTarget

写好之後,就可以使用了

Ajax form

过往我们常用js.erb 渲染非同步的表单,而当我们引入了Stimulus 以後,我们可以不用重新开一个js.erb的档案。

⭐️ 此例跟上例用的是一样的routes, controller,与上例的情境相同,只不过这边是使用Ajax送出表单的方式进行非同步,因此使用到data-remote=truehelper

import { Controller } from 'stimulus';

export default class extends Controller {
  static targets = ["searchedContent"]

  onBlogSuccess(event) {
    let [data, status, xhr] = event.detail;
    this.searchedContentTarget.innerHTML = xhr.response;
  }

  onBlogError(event) {
    let [data, status, xhr] = event.detail;
    console.log(xhr.response);
  }
}
= tag.div data: { controller: 'admin--blogs' } do
  / 中间内容省略...
  = modal(id: 'new-blog-modal', confirm_wording: '送出文章',
          confirm_form: 'new_modal', title: '新增文章') do
    / 中间内容省略...
    = simple_form_for(@blog, url: search_admin_blogs_path, method: :post,
                      html: { data: { remote: true,
                      action: "ajax:success->admin--blogs#onBlogSuccess 
                               ajax:error->admin--blogs#onBlogError" },
                      id: 'form2' }) do
    = tag.div style: "border: 1px solid black"
      = tag.div(class: 'form-group string required admin_blogs_search_id"')
        = tag.label tag.strong('搜寻id')
        = tag.input name: 'id', class: "form-control string required", form: 'form2'
      = submit_tag "搜寻", class: "btn btn-primary btn-sm", form: 'form2',
                          data: { disable_with: '载入中...' }
      = tag.div data: { 'admin--blogs-target': 'searchedContent' }

我们在Day25 提过下列的例子,但Day25的重点是讲述表单包覆表单的问题,今天的重点在於Ajax资料的打法。

对於上述的程序码,我们来进行解析

  • 在表单加入remote: true,代表该表单需要做非同步的处理

  • ajax:success->admin--blogs#onBlogSuccess ➡️ 当非同步进入成功阶段的反应

  • ajax:error->admin--blogs#onBlogError ➡️ 当非同步进入失败阶段的反应

其中onBlogSuccess, onBlogError 分别为在Stimulus自定义的两个动作。

结论

今天介绍了三种Stimulus 搭配非同步表单的例子,还在使用React, Vue的朋友们,汉汉老师想要告诉你们,Stimulus 这种基於SSR的框架很棒,一点也不逊於主流框架,并且目前在社群上已经有很多星星数不多,但实际上已经很好用的套件。

这几天不断地写文章,因此运动跟作息都变得比较不规律,偶尔会写到怀疑人生。写着写着,发现自己想要分享的内容比预期的还要多很多,但总觉得时间不够、文笔不够、实力不够,因此我们会在最後一天写下遗珠之憾,为下次的IT铁人赛做引言。

Rails 真的超棒!希望大家能够认识Rails

今天除了写了Day27以外,还回头顺Day1-5的文章。

参考资料


<<:  每个人都该学的30个Python技巧|技巧 27:常用的字串函式统整(字幕、衬乐、练习)

>>:  企划实现(12)

Leetcode 挑战 Day 08 [191. Number of 1 Bits]

191. Number of 1 Bits 今天这一题是有关於二进制的概念和其与十进位之间的转换,如...

另一个AAA(Yet Another AAA)-AAA Part I

访问控制机制 通常通过三种机制来管理或控制访问:身份验证,授权和会计(AAA)。 .身份验证是“验...

Flutter API Get using Bloc state management and http plugin

Flutter API Get using Bloc state management and ht...

Day 49 (Node.js)

1.express-session设定 var express = require('express...

[第十二天]从0开始的UnityAR手机游戏开发-如何在辨识图卡时拨放影片01

小试身手解答: 点击File→Save As... 跳出此视窗,将场景命名为ARVideo储存在此 ...