如何用 setter 替换 `@computed` 返回新的值和新的原生 setters?

How to replace `@computed` with setter returning new value with new native setters?

问题

我经常使用这种计算属性,其中 setter 只是 returns 新值:

  @computed('args.myValue')
  get myValue() {
    return this.args.myValue;
  }
  set myValue(newValue) {
    return newValue; // <==== this is no longer valid with native setter
  }

这没什么用:

  1. 将初始值设置为 args.myValue
  2. 允许更改值(通常通过 <Input @value={{this.myValue}} />
  3. args.myValue改变时恢复默认值

问题来自本机 setter,它不能 return 任何值。

请注意,我可能会找到一个“hackish”解决方案,但我希望代码遵循新的 EmberJS 约定,以避免以后更新时出现痛苦。

我尝试过的事情

手动缓存

  @tracked _myValue = null;

  get myValue() {
    return this._myValue || this.args.myValue;
  }
  set myValue(newValue) {
    this._myValue = newValue;
  }

这不起作用,因为 _myValue 总是设置在第一个 myValue=(newValue) 之后。 为了使其工作,应该有某种观察者在 args.myValue 更改时将其重置为 null

可悲的是,observers are no longer part of EmberJS with native classes

{{unbound}}帮手

<Input @value={{unbound this.myValue}} />

不出所料,它不起作用,因为它只是不更新​​ myValue

{{unbound}} helper 结合 event.target.value handling

<Input @value={{unbound this.myValue}} {{on "keyup" this.keyPressed}} />
  get myValue() {
    return this.args.myValue;
  }

  @action keyPressed(event) {
    this.doStuffThatWillUpdateAtSomeTimeMyValue(event.target.value);
  }

但是当args.myValue改变时Input仍然没有更新。

初始代码

这里有一个更具体的使用例子:

组件

// app/components/my-component.js

export default class MyComponent extends Component {

  @computed('args.projectName')
  get projectName() {
    return this.args.projectName;
  }
  set projectName(newValue) {
    return newValue; // <==== this is no longer valid with native setter
  }

  @action
  searchProjects() {
    /* event key stuff omitted */
    const query = this.projectName;
    this.args.queryProjects(query);
  }
}
{{! app/components/my-component.hbs }}

<Input @value={{this.projectName}} {{on "keyup" this.searchProjects}} />

控制器

// app/controllers/index.js

export default class IndexController extends Controller {

  get entry() {
    return this.model.entry;
  }

  get entryProjectName() {
    return this.entry.get('project.name');
  }

  @tracked queriedProjects = null;

  @action queryProjects(query) {
    this.store.query('project', { filter: { query: query } })
      .then((projects) => this.queriedProjects = projects);
  }

  @action setEntryProject(project) {
    this.entry.project = project;
  }
}
{{! app/templates/index.hbs }}

<MyComponent 
  @projectName={{this.entryProjectName}} 
  @searchProjects={{this.queryProjects}} />

当在控制器中设置queriedProjects时,组件显示它们。

单击其中一个搜索结果时,控制器会更新 setEntryProject 调用。

根据this Ember.js discussion

Net, my own view here is that for exactly this reason, it’s often better to use a regular <input> instead of the <Input> component, and to wire up your own event listeners. That will make you responsible to set the item.quantity value in the action, but it also eliminates that last problem of having two different ways of setting the same value, and it also gives you a chance to do other things with the event handling.

我通过使用标准 <input> 找到了这个问题的解决方案,这似乎是解决它的“正确方法”(我将非常感谢任何告诉我更好方法的评论):

{{! app/components/my-component.hbs }}

<input value={{this.projectName}} {{on "keyup" this.searchProjects}} />
// app/components/my-component.js

@action
searchProjects(event) {
  /* event key stuff omitted */
  const query = event.target.value;
  this.args.queryProjects(query);
}

如果我需要将输入值保持为 属性,我可以这样做:

{{! app/components/my-component.hbs }}

<input value={{this.projectName}} 
  {{on "input" this.setProjectQuery}} 
  {{on "keyup" this.searchProjects}} />
// app/components/my-component.js

@action setProjectQuery(event) {
  this._projectQuery = event.target.value;
}

@action
searchProjects( {
  /* event key stuff omitted */
  const query = this._projectQuery;
  this.args.queryProjects(query);
}

编辑

请注意以下解决方案有一个缺点:它没有提供一种简单的方法来将输入值重置为 this.projectName 当它不改变时,例如在 focusout 之后。

为了解决这个问题,我添加了一些代码:

{{! app/components/my-component.hbs }}

<input value={{or this.currentInputValue this.projectName}}
  {{on "focusin" this.setCurrentInputValue}}
  {{on "focusout" this.clearCurrentInputValue}}
  {{on "input" this.setProjectQuery}} 
  {{on "keyup" this.searchProjects}} />
// app/components/my-component.js
// previous code omitted

@tracked currentInputValue = null;

@action setCurrentInputValue() {
  this.currentInputValue = this.projectName;
}

@action clearCurrentInputValue() {
  this.currentInputValue = null;
}

对于这个 2 源绑定 场景有一个非常通用和简洁的方法,其中包含任何交互式输入元素及其他元素。

考虑您的第一次尝试(»手动缓存«):

  • 我们有一个通过 getter 和 setter 的功能反馈回路;不需要来自 setter 的 return 值,因为它无条件地触发绑定 getter (this._myValue 不需要被跟踪)
  • 需要一个开关来让一个变化的外部预设值(this.args.myValue)注入这个循环
  • 这是通过 GUID hashmap 实现的,该映射基于为交互式输入建立瞬时范围的预设值;因此,更改预设值注入和交互输入会相互覆盖:
// app/components/my-component.js
import Component from '@glimmer/component';
import { guidFor } from '@ember/object/internals';

export default class extends Component {

    // external preset value by @stringArg
    _myValue = new Map();
    
    get myValue() {
        let currentArg = this.args.stringArg || null;
        let guid = guidFor(currentArg);
        if (this._myValue.has(guid)) {
            return this._myValue.get(guid)
        }
        else {
            this._myValue.clear(); // (optional) avoid subsequent GUID reuse of primitive types (Strings)
            return currentArg;
        }
    }

    set myValue(value) {
        this._myValue.set(guidFor(this.args.stringArg || null), value);
    }
}

// app/components/my-component.hbs
<Input @value={{mut this.myValue}} />

https://ember-twiddle.com/a72fa70c472dfc54d03d040f0d849d17