返回

Laravel 表单更新:保持 Select 选项的旧值

php

好的,以下是一篇关于如何在 Laravel 中更新表单时保留选择框旧值的技术博客文章,该文章符合您的所有要求:


Laravel 表单更新:保持 Select 选项的旧值

在开发 Web 应用时,经常需要构建允许用户编辑数据的表单。用户提交表单后,应用验证并处理数据。处理过程中如遇验证错误,通常需将用户重定向回表单页,并显示先前的输入。

对于文本输入框(input type=text),这个过程相对容易。但是,对于选择框(select),情况会复杂些。默认情况下,表单重新加载后选择框会恢复到初始状态,用户需要重新选择,导致体验不佳。为了让 select 记住之前的选择项,本文讨论了相应的处理方式,避免不必要的信息丢失。

问题根源:表单回填机制

Laravel 框架本身不直接保留 select 元素的旧值。当我们遇到验证失败需要用户修正时,old() 辅助函数用于将表单的先前输入重新填充到表单字段中。old() 在重新填充文本输入方面十分有效,然而默认地, old() 函数只负责提供回填字段的数据,而将数据应用到HTML这个步骤,是由开发人员手动处理的。old() 函数需要与 input 标签的 value 属性或其他 HTML 标签属性结合使用。例如:

<input type="text" name="username" value="{{ old('username') }}">

select的旧值同样通过 old() 辅助函数获得,而让其生效还需要为 option 标签的 selected 属性赋与特定的值:

<option value="your_value" {{--如果 $your_value 等于old('filed'),则会被选中--}}selected>
</option>

但这里通常与用户的选择和原始数据库中的选择都有关,因此需要在后续判断。

当没有可用的旧值时,默认选项会按 HTML 代码逻辑中的方式来显示。

解决方案详解

这里介绍三种通用的方案,每一种都对应不同的使用场景:

1. 使用三元运算符结合 old() 辅助函数

这个方法直接将旧值、模型值和 option 值在 HTMLoption 标签中通过三元运算符比较,符合则应用 selected 属性。这是非常直观的方法。

操作步骤:

  1. option 标签的 selected 属性内进行逻辑判断,并插入相应的 selected 值。
  2. 判断逻辑要对用户修改选择的结果和 DB 中现有的选择做相应的覆盖处理,使用户的旧选择 old('file') 优先应用到 selected 中。
  3. 请参照如下的代码编写符合场景的处理逻辑。

代码示例:

<select class="form-select" name="slug" id="slug">
    <option value="">Select Category </option>
    <option value="commercial" {{ old('slug', $project->slug ?? null) == 'commercial' ? 'selected' : '' }}>commercial</option>
    <option value="residential" {{ old('slug', $project->slug ?? null) == 'residential' ? 'selected' : '' }}>residential</option>
    <option value="industrial" {{ old('slug', $project->slug ?? null) == 'industrial' ? 'selected' : '' }}>industrial</option>
</select>

原理说明:

代码中的 old('slug', $project->slug ?? null) 表示,如果有旧的 slug 输入值(在验证失败的情况下),则使用旧值,否则使用 $project->slug 的值(数据库中的值),如果 $project->slug 不存在,则使用 null。三元运算符用于判断 old() 函数提供的旧值或 $project->slug 值是否与当前 option 的值相同。如果相同,则为该 option 添加 selected 属性,表示该选项为选定状态。当没有任何选项匹配到值时,第一个option元素作为 select 的默认选中项(通常显示为一个空提示)。

2. 自定义 Blade 指令,并针对每个页面单独编写JavaScript函数进行动态更新

针对频繁处理这类选择项的应用来说,自定义 Blade 指令是一个效率高的做法。这需要定义自定义指令、引入并运行 JavaScript 代码。步骤看起来更多一些,却值得推广到长期实践中,让后续的使用和复用更便利。

操作步骤:

  1. AppServiceProvider 中定义一个新的 Blade 指令。该指令将在处理 select 表单元素时调用 JS 中的一个特定方法去为相应的 option 附加 selected 属性。
  2. 根据 select 中用到的实际业务逻辑编写这个 JavaScript 函数,它通常是一个闭包。该函数必须确保能接收三个参数:select的旧值、模型的现有值,以及当前 option 元素的 value 值。比较前两者是否与当前 option 值一致,如果是,则添加 selected 属性。
  3. 编写 Blade 模板,调用定义好的 Blade 指令。并提供给 JS 函数三个需要的参数。

代码示例:

步骤 1:在 AppServiceProvider 中添加 Blade 指令,以调用后续引入的 Javascript 函数。

// app/Providers/AppServiceProvider.php

use Illuminate\Support\Facades\Blade;

// ...

public function boot()
{
    Blade::directive('selectOption', function ($expression) {
        // 添加双引号以匹配原代码格式。这会让处理字符串表达式更简便,且更统一地让php使用更简便的方式来解释字符串内容。
        return "<?php echo selectOptionHelper(\"$expression\"); ?>";
    });
}

安全性说明:这里自定义的 selectOption 函数应避免接受除预期参数(选项值、原值、旧值)外的其他参数。通过限制传递参数的数量来防止意外传递了错误参数。并在文档中提供清晰的使用说明。此外在必要情况下进行服务器端校验,在服务端校验由selectOption提供的输入数据。通过参数白名单来限制只能输入某些值。

步骤 2: 创建相关的 Javascript 函数。

// public/js/app.js
function selectOptionHelper(selectId, modelValue, optionValue) {
  var selectElement = document.getElementById(selectId);
  var oldValue = selectElement.getAttribute('data-old-value'); // 获取data属性

  // 优先使用旧值,然后使用模型值。如果没有就返回空字符串
  var selectedValue = oldValue !== null && oldValue !== '' ? oldValue : (modelValue || '');

  // 使用forEach来循环为 option 应用逻辑,并给符合值的 option 添加 selected 属性。
  Array.from(selectElement.options).forEach(function(option) {
    if (option.value === selectedValue) {
      option.selected = true;
    } else {
      option.selected = false;
    }
  });

  return '';
}

安全性说明:selectOptionHelper通过option.value来匹配选中的值。确保仅接受预期的输入值和比较方式。对于option元素也同理地处理来增强安全性,如避免处理除预期数量外的option值。如有需要还可以对用户的输入在服务端也进行过滤和校验。以及采用适当的内容安全策略(CSP),可以添加内容安全策略来对HTML中的JavaScript进行额外的加固。使用nonce属性可以定义运行指定脚本的行为,而非运行所有的页面脚本,因此限制了范围和风险。

步骤 3: 在 Blade 模板中使用 JS 函数。

<select class="form-select" name="slug" id="slug" data-old-value="{{ old('slug') }}">
    <option value="">Select Category </option>
    <option value="commercial" @selectOption('slug', '{{ $project->slug ?? null }}', 'commercial')>commercial</option>
    <option value="residential" @selectOption('slug', '{{ $project->slug ?? null }}', 'residential')>residential</option>
    <option value="industrial" @selectOption('slug', '{{ $project->slug ?? null }}', 'industrial')>industrial</option>
</select>

<script src="{{ asset('js/app.js') }}"></script>

该例通过把old('slug') 存放在selectdata属性data-old-value中传递给 selectOptionHelper,让JavaScript能在DOM加载完成后更新选项的状态。然后将项目 model 中存好的项目状态传递过去作为 $project->slug ?? null 参数,再让该参数传递给 JS 函数,用于选项回退到的默认选中状态。同时还需要提供当前 option 元素的值(commercialresidentialindustrial)。三个条件符合其一时则该选项会设置 selected 。该方案使用了 JS 的能力去执行比较逻辑,可以覆盖很多复杂的情况,而且可以在JS脚本内部处理得非常干净,利于单元测试。但要注意代码加载顺序。通过这个方法可以将很多PHP的代码负担移除,这可以让 Blade 文件更容易理解,并提升维护效率。
原理说明:

此方法通过自定义 Blade 指令 selectOption 调用 JS 函数 selectOptionHelper 来比较并选择特定选项,该函数处理选项是否应该被选中。如果提交表单遇到错误,在 select 标签里 data-old-value 会利用 Laravelold() 辅助函数得到用户 session 中存储的上一次提交的值。如果一切运行正常则为 null,这个行为会在 JavaScript 函数中控制选项。然后使用模型值进行比较,决定某个 option 元素在其他情况下是否应该被设置为 selected
安全性建议:由于此处涉及到数据比较,需要在数据比较时做好参数和类型的匹配,尤其注意弱类型转换带来的意外情况。并遵循数据编码和数据转义来确保在JavaScript脚本中使用服务器端传输的数据前将其做好编码和解码。以确保特殊字符在处理或使用过程中没有产生意外的结果,从而避免各种常见的代码执行错误。

3. 通过Form Request进行后端处理。

使用请求类,我们可以更好地把处理用户数据的代码从 controller 代码中分离出来,这会让后续维护更容易。因此针对这种类型的 select 更新我们也可以使用这个模式去创建单独的处理类来解决该问题,而 controller 保持原样。

操作步骤:

  1. 生成新的 Form Request 类。
  2. 更新控制器方法。
  3. 添加 Form Request 类来专门处理该业务中相关的 Select 选项的请求内容。

代码示例:

步骤 1: 使用 Artisan 命令生成一个新的 Form Request:

php artisan make:request UpdateProjectRequest

步骤 2: 修改控制器方法以使用新的 Form Request:

// app/Http/Controllers/ProjectController.php

public function update(UpdateProjectRequest $request, Project $project)
{
    $validated = $request->validated();
    
    // 通过 `$validated` 来更新模型,处理业务逻辑
    $project->update($validated);

    // 重定向到某个路由并显示成功信息
    return redirect()->route('projects.show', $project)->with('success', 'Project updated successfully');
}

由于表单中的数据都在这个 UpdateProjectRequest 对象中处理,控制器的职责就被大幅缩减,而只需要考虑表单中的合法数据的应用即可。

步骤 3: 编写 Form Request 以处理 Select 选项的数据请求:

// app/Http/Requests/UpdateProjectRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateProjectRequest extends FormRequest
{
    public function authorize()
    {
        // 确保用户已获得授权能操作这些资源
        return true;
    }

    public function rules()
    {
        // 为表单定义通用的校验规则。如必填,最大值等等。
        // 我们使用 FormRequest 机制,就是让开发者有选择在这里去校验数据的类型和完整性。然后由框架告知开发人员校验通过与否,无需用户写冗余的代码去校验。
        // 如果没有这方面的校验,那么可以留空,仅处理select字段的处理。
        return [
            'name' => 'required|max:255',
            'slug' => 'sometimes',
            // ... 其它字段的校验规则
        ];
    }

    public function prepareForValidation()
    {
        // 使用 Form Request 内部的这个机制去调整提交表单的用户的数据,为用户修正一些简单的输入问题。这是把复杂的问题从业务代码和模板代码中解耦出来的机制,让不同
        // 模块能各司其职,而非在Controller方法或者Blade文件中做过多的判断,或引入过多的外部方法调用,从而更好地保证项目的长期健康度,使代码更整洁、更可读、更可测。
        
        // 我们使用合并输入值的方式去判断这个select选项。因为这个slug字段可能没有被输入,我们需要考虑更完整的情况,所以使用了如下方案
        $this->merge([
            'slug' => $this->input('slug', $this->project->slug ?? ''),
        ]);
    }
}

在该 FormRequest 的 prepareForValidation 方法中,$this->input('slug', $this->project->slug) 表示:

  • $this->input('slug'):尝试获取名为 slug 的输入值。当用户选择了某个选项,该方法会返回用户选择的值;否则,返回 null
  • $this->project->slug:访问当前请求关联的 Project 模型实例上的 slug 属性。这用于获取数据库中当前项目记录的 slug 值。如果之前没有设置过 slug,该表达式的值可能是 null
  • $this->project->slug ?? '':当访问的项目属性是 null 时提供默认的空字符串行为。这使得无论 project 实例上的 $this->project 为何值时都将 merge 一个非 null 结果。prepareForValidation 会在数据提交到用户业务方法前更新掉可能导致意外的值。

这个方法避免用户选择提交 null 到后续步骤,而是把这个判断的工作移到该 Request 对象中处理,保证了select提交内容的可维护性,还避免用户选择一个空选项带来的业务错误,这是一个处理 select 提交请求的方式。使用这种模式时可以考虑引入专门的方法处理这类选项。将代码分散在更小的逻辑中进行管理能大幅提升工作效率和测试效率。prepareForValidation 同样也避免了模板中过多的调用,或PHP文件中冗余的代码。因此,它也可以更好地提升Web应用的安全性。在 Form Request 中通过使用这个钩子处理输入值也能更容易地把校验代码集中起来管理。可以更直接地进行后续维护。当需要维护验证逻辑的时候,就无须修改很多处业务代码或模板代码。这提高了应用安全相关的效率。通过这样的方式能减少在各个模块之间重复编写和应用同类安全处理的代码。通过集中维护还可降低不同代码模块之间因不同版本的数据修改导致的不同步的输入处理风险。这种做法在更清晰、可控性更强、安全管理效率更高的项目中会更经常被运用。
原理说明:

在请求过程中,首先进行表单验证(FormRequest 中的 rules)。在执行这些验证规则前执行 prepareForValidation 里的 merge 方法。将请求的数据(或 $project->slug 的值或 '' )与验证数据做一次合并,以符合预期。

该方案不影响原有逻辑,但需要保证控制器的相应方法里能够传入表单中所有的数据,而非特定选择的数据。

通过使用 Form Request 来调整表单中的select选项的数据。把需要保持的 select 的数据从 ControllerBlade 代码中移动到了特定的请求类中进行管理。避免select提交一个无效的值,在处理旧选项和数据库现有的值冲突的时候,更安全,可靠,也更清晰,在可维护性方面也有明显的优势。如果业务需求中,还需要调整其他类似 select 的组件,该方式还可以帮助项目更好地规避因为更新select组件的行为不一致,或者忘记更新 Blade 模板中 select 选项的处理造成的业务风险或bug,让后续项目能更容易地迭代下去。

总结

以上介绍的三种解决思路中第一种适用小型项目或表单变动不大的场景。而第二种通过创建 Blade 指令的方式能够一劳永逸,如果项目中需要大量使用这类表单。那么通过引入新的 Javascript 方法是一个较好的工程实践。这减少了不同的人处理不同页面 select 标签时产生不同的bug。长期维护项目的负担会更低一些。第三种针对需要对 select 组件进行单独管理时更友好。使用 Form Request 来单独地管理这个请求的数据,允许将处理代码从模板代码、业务代码中剥离,在进行项目安全性加固、安全审核时也会更直接。该方法也适用于有专人管理不同类 select 数据的业务需求,以及项目成员对安全类的要求。根据项目的规模、业务要求和项目组成员的能力不同,可以在这三个方法中选一个长期执行下去。以上方案中的三个安全建议也有相应的适用场景,应该根据业务规模不同灵活采纳。