在Alpine.js中构建相关选择的详细指南(附代码)

156 阅读3分钟

在网页设计中,一个常见的UX/UI隐喻是 "相关 "选择或下拉菜单的想法。我的意思是,有一个选项的选择字段,当你从那里选择了什么,它就会驱动另一个(或相关)选择字段的内容。这方面的一个例子是一个下拉式的汽车品牌。当选择了一个特定的品牌,你就会得到一个下拉的汽车模型。我想在Alpine.js中建立一个这样的例子会很有趣。

首先,我创建了两个实用函数,为我的选择创建模拟数据。第一个函数返回一个状态列表(不是所有的状态,这也没关系):

const getStates = () => {
    return [
        {id:1, label:"Alabama"},
        {id:2, label:"California"},
        {id:3, label:"Louisiana"},
        {id:4, label:"Texas"},
        {id:5, label:"Washington"}
        ]
}

注意每个州都有一个idlabel 值。每个州都有一组相关的城市。为此,我建立了一个方法来在请求时生成它们。为了演示为每个州返回不同的数据集,城市的名称和城市的数量都是动态的:

const getCities = (id) => {
    if(!id) return [];
    let state = (getStates()).find(i => i.id === parseInt(id,10));
    let result = [];
    for(let i=0; i<(id*2); i++) {
        result.push({id:i, label:`${state.label} City ${i+1}`});
    }
    return result;
}

好吧,让我们看看我将用于此的HTML:

<div x-data="app" x-cloak>
    <label>State:
    <select x-model="state">
        <option value="">-- Select a State --</option>
        <template x-for="state in states">
            <option :value="state.id"><span x-text="state.label"></span></option>
        </template>
    </select>
    </label>
    
    <label x-show="state">City:
        <select x-model="city">
            <template x-for="city in cities">
                <option :value="city.id"><span x-text="city.label"></span></option>
            </template>
        </select>
    </label>
</div>

我有两个主要部分,一个是州,一个是城市。对于州,我已经将选择绑定到一个state 变量上,而选项来自states

对于城市,它非常相似,但注意到使用了x-show 。这里的想法是,只有当选择了一个州时,才会显示城市下拉。

现在让我们来看看JavaScript:

document.addEventListener('alpine:init', () => {
  Alpine.data('app', () => ({
        states:getStates(),
        state:null,
        city:null,
        cities() {
            return getCities(this.state);
        }
  }))
});

由于这是一个非常简单的演示,代码并不长。你可以看到states ,利用了我之前定义的函数。同理,cities ,但注意它是如何使用this.state 。这将在state 的值发生变化时重新运行。我不需要做任何其他事情。你可以在这里自己玩这个。

为了好玩,我决定建立另一个版本,这个版本中对一个状态的城市的调用是异步的。你可以想象这是调用API的结果,例如。作为一个伪造的黑客方法,我简单地使用了setTimeout 。这是新的版本:

const getCities = async (id) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(!id) resolve([]);
            let state = (getStates()).find(i => i.id === parseInt(id,10));
            let result = [];
            for(let i=0; i<(id*2); i++) {
                result.push({id:i, label:`${state.label} City ${i+1}`});
            }
            resolve(result);
        }, 1000);
    });
}

还有一点不同的是,我检查id 的值是否为空,它在加载时将是空的,如果是,则返回一个空数组。

现在--起初--使用这个是很容易修改的。我只是在我的Alpine应用程序中对cities 的定义做了这个修改:

async cities() {
    return  await getCities(this.state);
}

它就这样神奇地工作了。但是,当一个状态被选中时,并没有任何反馈给用户,说明有事情发生。我决定添加一个加载信息,并确保在这个过程中隐藏城市选择。这最终变得有点复杂了。

首先,我修改了我的HTML的后半部分:

<span x-show="loadingCities"><i>Loading cities...</i></span>
<label x-show="state && !loadingCities">
    <span x-show="!loadingCities">City:</span>
    <select x-model="city" x-show="!loadingCities">
        <template x-for="city in cities">
            <option :value="city.id"><span x-text="city.label"></span></option>
        </template>
    </select>
</label>

首先,我有一个跨度,当一个新的值,loadingCities ,为真时显示出来。稍后你会看到这个。然后我修改了label ,以检查stateloadingCities 是否为假。到目前为止还不错。但事情在这里变得很奇怪。在我看来,这应该已经足够了。但是,当我把 "City "单独放在一个跨度中时,即使loadingCities 是真的,我也会在第一次选择一个状态时看到它。在第二次,诸如此类的选择中,它像预期的那样工作。我还遇到一个问题,就是选择显示为空。

我不知道为什么,但我添加了跨度,在里面使用了x-show ,尽管感觉我做的工作比预期的要多,但似乎效果不错。

下面是更新后的JavaScript:

document.addEventListener('alpine:init', () => {
  Alpine.data('app', () => ({
        states:getStates(),
        state:null,
        loadingCities:false,
        async cities() {
            if(this.state) this.loadingCities = true;
            let result = await getCities(this.state);
            this.loadingCities = false;
            return result;
        }
  }))
});

你可以看到cities 是比较复杂的。我得先得到结果,然后把我的加载值设回为假,再返回。总而言之,它起作用了,但正如我所说的,要把所有的时间安排好是很棘手的。这里有这个应用程序供你玩。