js 打字机

    科技2026-08-18  20

    js 打字机

    I needed a typeahead for a project. Not just any typeahead, mind you, I need a typeahead for US state & county. I need to be able to positively select a US state and county within that state, and do so easily. Something a bit like this:

    我需要为项目提前输入。 不是任何预输入,你要知道,我需要为美国州和县预输入。 我需要能够肯定地选择一个美国州和该州内的县,并轻松地做到这一点。 有点像这样:

    When I select a state & county, I don’t want the typeahead’s value to be some monstrous string that I then have to parse to get the state & county back out of it, I want an object that has those fields on it.

    当我选择州和县时,我不希望预输入的值是一些难以理解的字符串,然后我必须解析该字符串才能使州和县摆脱它,我想要一个带有这些字段的对象。

    Here are some of the concerns that I have to wrangle:

    以下是我必须解决的一些问题:

    Wait until we’ve pulled the state & county data from the server before displaying the UI (i.e. display “Loading…” or something)

    等待直到我们从服务器中提取州和县数据,然后再显示UI(即显示“正在加载...”或类似内容) Compute suggestions on each keystroke

    计算每次击键的建议 Move the highlighted selection on up & down keystrokes

    上下按键移动突出显示的选择Show a “selected” UI when a selection has been made

    做出选择后显示“选择的” UIOnly show the suggestions when the input box has focus

    仅在输入框具有焦点时显示建议

    If you think about this typeahead, its behavior can be modeled by a humble state machine. Here’s how we’ll model the states of this typeahead:

    如果您考虑这种提前输入,则其行为可以由状态机来模拟。 这是我们为这种提前状态建模的方法:

    State diagram for the picker widget. 选择器小部件的状态图。

    在打字稿中建模状态机 (Modeling a State Machine in Typescript)

    In a functional language like F# or Haskell, we can use a discriminated union (DU) to represent each state and the data that pertains to each state. Typescript, however, doesn’t have DUs, so we have to find a way to wield Typescript’s type system to help us model this state machine. We’ll use types to represent each state— Initial, ReadyForInput, SuggestionsVisible, and Selected— and also, any relevant data that is pertinent to each state.

    在F#或Haskell之类的功能语言中,我们可以使用区分联合(DU)来表示每个状态以及与每个状态有关的数据。 但是,Typescript没有DU,因此我们必须找到一种使用Typescript的类型系统的方法来帮助我们对该状态机进行建模。 我们将使用类型来表示每个状态,包括Initial , ReadyForInput , SuggestionsVisible和Selected ,以及与每个状态相关的任何相关数据。

    Next, we have to find a way to represent a type in Typescript that is both the union of all these types, and also has an easy way to tell which kind it is. We can borrow a concept from DUs (also called “tagged unions”) and create a tag:

    接下来,我们必须找到一种在Typescript中表示类型的方法,该方法既是所有这些类型的并集,又有一种简单的方法来判断它是哪种类型。 我们可以从DU(也称为“标记的联合”)中借用一个概念并创建一个标签:

    import StateCounty from "./models/StateCounty" type Tag = { tag: "Initial" | "ReadyForInput" | "SuggestionsVisible" | "Selected"; } export type Initial = Tag & { tag: "Initial" }; export type ReadyForInput = Tag & { tag: "ReadyForInput"; allStateCounties: StateCounty[]; text: string; } export type SuggestionsVisible = Tag & { tag: "SuggestionsVisible"; allStateCounties: StateCounty[]; text: string; suggestions: StateCounty[]; suggestionIndex: number; } export type Selected = Tag & { tag: "Selected"; allStateCounties: StateCounty[]; selection: StateCounty; } export type State = Initial | ReadyForInput | SuggestionsVisible | Selected;

    Now, when you create a new instance of any of the exported types, it has a tag field that must be the string value that corresponds to the type name:"Initial", "ReadyForInput", "SuggestionsVisible", or "Selected". And finally, we define our State type to be one of the four individual state types. What’s handy about this is that the compiler enforces compliance to these type definitions. You can’t have a State unless it complies with these types, and you can easily tell them apart by the tag field when you need to figure out which state it is.

    现在,当您创建任何导出类型的新实例时,它都有一个tag字段,该字段必须是与类型名称相对应的字符串值: "Initial" , "ReadyForInput" , "SuggestionsVisible"或"Selected" 。 最后,我们将State类型定义为四种单独州类型之一。 方便的是,编译器强制遵守这些类型定义。 除非它符合这些类型,否则您就无法拥有State ,并且在需要确定州属于哪种状态时,可以通过tag字段轻松区分它们。

    We then do something similar for the actions, which represent the transitions between our state machine’s states:

    然后,我们对action进行类似的操作,这些操作代表状态机状态之间的转换:

    import StateCounty from "./models/StateCounty"; type Tag = { tag: "LoadData" | "GotFocus" | "LostFocus" | "Select" | "ClearSelection" | "TextChanged" | "MoveHighlightUp" | "MoveHighlightDown"; } export type LoadData = Tag & { tag: "LoadData" stateCounties: StateCounty[]; } export type GotFocus = Tag & { tag: "GotFocus" } export type LostFocus = Tag & { tag: "LostFocus" } export type Select = Tag & { tag: "Select" } export type ClearSelection = Tag & { tag: "ClearSelection" } export type TextChanged = Tag & { tag: "TextChanged"; text: string; } export type MoveHighlightUp = Tag & { tag: "MoveHighlightUp" } export type MoveHighlightDown = Tag & { tag: "MoveHighlightDown" } export type Action = LoadData | GotFocus | LostFocus | Select | ClearSelection | TextChanged | MoveHighlightUp | MoveHighlightDown;

    Next, we’re going to use an interesting feature of Typescript: type guards. You can define a function that narrows Typescript’s interpretation of a variable. We can use these tag values to simplify how we define these type guards:

    接下来,我们将使用Typescript的一个有趣功能: type guards 。 您可以定义一个缩小Typescript对变量的解释的函数。 我们可以使用这些标记值来简化定义这些类型防护的方式:

    export function isTextChanged(x: Action) : x is TextChanged { return x.tag === "TextChanged"; }

    Notice the return type, x is LoadData. This is a type predicate. We can use these type guards similarly to how a functional language might use pattern matching:

    注意返回类型, x is LoadData 。 这是一个类型谓词。 我们可以像功能语言使用模式匹配一​​样使用这些类型防护:

    if(isTextChanged(action)) { console.log(action.text); // in this scope, action is a TextChanged } else { // action is still just Action - not more specific }

    On line 2 above, Typescript has narrowed the action variable to be TextChanged in the scope of the if statement.

    在上面的第2行中,Typescript在if语句的范围内将action变量的范围缩小为TextChanged 。

    好的,老兄,我已经无聊了! (Okay, jeez, I’m bored already!)

    Hang in there! There’s a reason why we’ve set all these types up. We can now drive a state machine using a reducer function with a signature like this:

    挂在那里! 我们设置所有这些类型是有原因的。 现在,我们可以使用带有签名的reducer函数来驱动状态机:

    type Reducer = (action: Action, state: State) => State;

    The beauty of having set up all these types and type guards is that now the compiler will prevent us from breaking our state machine: passing it invalid data will be a compiler error. This also makes writing the reducer function really straightforward:

    设置所有这些类型和类型防护的好处在于,现在编译器将阻止我们破坏状态机:将无效数据传递给它会导致编译器错误。 这也使得编写reducer函数非常简单:

    const reduceInitial = (action : Action, state: Initial) => { if(Actions.isLoadData(action)) { return <ReadyForInput>{ tag: "ReadyForInput", allStateCounties: action.stateCounties, text: "" } } return state; } const reducer : Reducer = (action, state) => { if(States.isInitial(state)) { return reduceInitial(action, state); } return state; }

    For each state, we write a smaller reducer that only cares about the transitions from that state to another state, like reduceInitial above. We can compose reducers for each state, like this:

    对于每个状态,我们编写一个较小的化reduceInitial器,仅关心从该状态到另一状态的过渡,例如上面的reduceInitial 。 我们可以为每个状态组成reducer,如下所示:

    const reducer : Reducer = (action, state) => { if(States.isInitial(state)) { return reduceInitial(action, state); } else if(States.isReadyForInput(state)) { return reduceReadyForInput(action, state); } else if(States.isSuggestionsVisible(state)) { return reduceSuggestionsVisible(action, state); } else if(States.isSelected(state)) { return reduceSelected(action, state); } return state; }

    Now, we can easily address each transition (arrows from the state diagram) from each state. What’s more: this automatically ignores irrelevant actions.

    现在,我们可以轻松解决每个状态的每个过渡(状态图中的箭头)。 更重要的是:这会自动忽略无关的操作。

    好吧,现在呢? (Okay, Now What?)

    Now, we can easily write unit tests for states and transitions. For instance, in the SuggestionsVisible state, we want to be able to press the up and down arrows to change which selection is highlighted (suggestionIndex), but we want to prevent that suggestionIndex from going out-of-bounds if you press up at the top of the list or down at the bottom of the list:

    现在,我们可以轻松编写状态和转换的单元测试。 例如,在“ SuggestionsVisible状态下,我们希望能够按向上和向下箭头更改突出显示的选择项(“ suggestionIndex ),但是如果您在菜单上按向上键,我们希望防止该suggestionIndex越界。列表顶部或列表底部:

    describe("SuggestionsVisible state", () => { const state = <SuggestionsVisible>{ tag: "SuggestionsVisible", allStateCounties: stateCounties, // generated elsewhere, omitted for brevity suggestionIndex: 0, suggestions: stateCounties.slice(0, 3), text: 'text', }; it("should handle move down action", () => { const expected = { ...state, suggestionIndex: 1 }; const actual = reducer({ tag: "MoveHighlightDown" }, state); expect(actual).toEqual(expected); }); it("should handle move up action", () => { const startState = { ...state, suggestionIndex: 1 }; const actual = reducer({ tag: "MoveHighlightUp" }, state); expect(actual).toEqual(state); }); it("should ignore move up action at suggestionIndex 0", () => { const actual = reducer({ tag: "MoveHighlightUp" }, state); expect(actual).toEqual(state); }); it("should ignore move down action at bottom of suggestions", () => { const startState = { ...state, suggestionIndex: state.suggestions.length - 1 }; const actual = reducer({ tag: "MoveHighlightDown" }, startState); expect(actual).toEqual(startState); }); }) // and the implementation const reduceSuggestionsVisible = (action : Action, state: States.SuggestionsVisible) => { if(Actions.isMoveHighlightDown(action)) { if(state.suggestionIndex < (state.suggestions.length - 1)) { return { ...state, suggestionIndex: state.suggestionIndex + 1 } } } else if(Actions.isMoveHighlightUp(action)) { if(state.suggestionIndex > 0) { return { ...state, suggestionIndex: state.suggestionIndex - 1 } } } return state; }

    Following this pattern, we can define our reducer so that it accepts only the actions that are relevant to whatever state we’re in, ignoring irrelevant actions. The compiler enforces that we can only pass valid data, and the code and tests are easy to read and write.

    按照这种模式,我们可以定义化简器,使其仅接受与我们所处状态无关的动作,而忽略不相关的动作。 编译器强制我们只能传递有效数据,并且代码和测试易于读取和编写。

    Hope this helps you. Happy coding!

    希望对您有帮助。 编码愉快!

    翻译自: https://medium.com/@floyd.may/building-a-typescript-state-machine-cc9e55995fa8

    js 打字机

    相关资源:javascript经典特效---状态栏打字机.rar
    Processed: 0.014, SQL: 9