在Angular中,如何将模型绑定到值为null、undefined或空字符串的选项?

huangapple go评论78阅读模式
英文:

In angular how to bind model with value of null or undefined or empty string to option in select?

问题

以下是翻译的部分:

我有一个名为 model 的变量。这个变量可以是 nullundefined、空字符串,或者有效的带有值的字符串或对象。

class .. {
   model = null // undefined or ''
}

这个 select 绑定到了 model 变量上:[(ngModel)]="model"

在这个 select 中有一些选项,其中一个选项是 select..(没有值)。

我想要的是:如果 modelnullundefined 或空字符串 - select 控件将选择第一个选项(没有值)。

<select [(ngModel)]="model">
  <option>选择某物...</option>
  <option value="1">选项1</option>
  <option value="2">选项2</option>
</select>

当我将值设置为 null 并且 model 为 null 时,它会匹配到 选择某物 选项:

<option [value]="null">..

但是当模型更改为 undefined 时,什么都不匹配 - 我想要匹配 选择某物...

所以我考虑自己制作 option 指令 的版本,并复制 option 在设置值时的行为。

问题是我尝试设置值,但它并没有按预期工作。

this._renderer.setProperty(this._element.nativeElement, 'value', ''); // null/undefined/''

有没有办法使这个工作?

以下是您提供的代码示例:

@Directive({ selector: '[ngxValue]', standalone: true })
export class NgSelectOption implements OnDestroy {
  id!: string;

  constructor(
    private _element: ElementRef,
    private _renderer: Renderer2,
    @Optional() @Host() private _select: SelectControlValueAccessor
  ) {
    if (this._select) this.id = (this._select as any)._registerOption();
  }

  @Input('ngxValue')
  set ngxValue(value: any) {
    console.log({ value });
    this._renderer.setProperty(this._element.nativeElement, 'value', '');
  }

  _setElementValue(value: string): void {
    this._renderer.setProperty(this._element.nativeElement, 'value', value);
  }

  ngOnDestroy(): void {
    if (this._select) {
      (this._select as any)._optionMap.delete(this.id);
      this._select.writeValue(this._select.value);
    }
  }
}

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, FormsModule, NgSelectOption],
  template: `
    <h1>Hello from {{name}}!</h1>
    <a target="_blank" href="https://angular.io/start">
      Learn more about Angular
    </a><br>
    <select [(ngModel)]="model_undefined">
      <option disabled ngxValue>选择某物...</option>
      <option [value]="1">选项1</option>
    </select>
    model_undefined: {{model_undefined|json}}
    <br>
    <select [(ngModel)]="model_null">
      <option disabled ngxValue>选择某物...</option>
      <option [value]="1">选项1</option>
    </select>
    model_null: {{model_null|json}}
    <br>
    <select [(ngModel)]="model_empty_string">
      <option disabled ngxValue>选择某物...</option>
      <option [value]="1">选项1</option>
    </select>
    model_empty_string: {{model_empty_string|json}}
    <br>
    <select [(ngModel)]="mode_with_value">
      <option disabled ngxValue>选择某物...</option>
      <option [value]="1">选项1</option>
    </select>
    mode_with_value: {{mode_with_value|json}}
  `,
})
export class App {
  name = 'Angular';

  model_undefined = undefined;
  model_null = null;
  model_empty_string = '';
  mode_with_value = '1';
}

stackblitz

英文:

I have variable called model. This variable can be null or undefined or empty string or valid with value: a string or object.

 class .. { 
model = null // undefined or &#39;&#39; or &#39;bla&#39;
}

The select is bind to this model variable: [(ngModel)]=&quot;model&quot;.

In the select there are some options and one option is select.. (with no value).

What I want is: If the model is null or undefined or empty string - the select control will select the first option (without value).

 &lt;select [(ngModel)]=&quot;model&quot;&gt;
&lt;option&gt;select something...&lt;/option&gt;
&lt;option value=&quot;1&quot;&gt;op-1&lt;/option&gt;
&lt;option value=&quot;2&quot;&gt;op-2&lt;/option&gt;
&lt;/select&gt;

When I set the option with value of null and the model is null, then it match the option of select something:

 &lt;option [value]=&quot;null&quot;&gt;..

But when the model is changing to undefined then nothing is match- and what I want is to match the select something....

So I was thinking to do my own version of option directive and copy the behavior of option when setting the value.

The problem is I try to set the value but it doesn't change as excepted.

    this._renderer.setProperty(this._element.nativeElement, &#39;value&#39;, &#39;&#39;); // null/undefiend/&#39;&#39;

Any idea how to make this work?

Here my code I try to play with:


@Directive({ selector: &#39;[ngxValue]&#39;, standalone: true })
export class NgSelectOption implements OnDestroy {
/**
* @description
* ID of the option element
*/
// TODO(issue/24571): remove &#39;!&#39;.
id!: string;
constructor(
private _element: ElementRef,
private _renderer: Renderer2,
@Optional() @Host() private _select: SelectControlValueAccessor
) {
if (this._select) this.id = (this._select as any)._registerOption();
}
// @Input(&#39;ngValue&#39;)
// set ngValue(value: any) {
//   if (this._select == null) return;
//   this._select._optionMap.set(this.id, value);
//   this._setElementValue(_buildValueString(this.id, value));
//   this._select.writeValue(this._select.value);
// }
// @Input(&#39;value&#39;)
// set value(value: any) {
//   this._setElementValue(value);
//   if (this._select) this._select.writeValue(this._select.value);
// }
@Input(&#39;ngxValue&#39;)
set ngxValue(value: any) {
console.log({ value });
this._renderer.setProperty(this._element.nativeElement, &#39;value&#39;, &#39;&#39;);
}
/** @internal */
_setElementValue(value: string): void {
this._renderer.setProperty(this._element.nativeElement, &#39;value&#39;, value);
}
/** @nodoc */
ngOnDestroy(): void {
if (this._select) {
(this._select as any)._optionMap.delete(this.id);
this._select.writeValue(this._select.value);
}
}
}
@Component({
selector: &#39;my-app&#39;,
standalone: true,
imports: [CommonModule, FormsModule, NgSelectOption],
template: `
&lt;h1&gt;Hello from {{name}}!&lt;/h1&gt;
&lt;a target=&quot;_blank&quot; href=&quot;https://angular.io/start&quot;&gt;
Learn more about Angular 
&lt;/a&gt;&lt;br&gt;
&lt;select [(ngModel)]=&quot;model_undefined&quot;&gt;
&lt;option disabled ngxValue&gt;select something...&lt;/option&gt;
&lt;option [value]=&quot;1&quot;&gt;op1&lt;/option&gt;
&lt;/select&gt;
model_undefined: {{model_undefined|json}}
&lt;br&gt;
&lt;select [(ngModel)]=&quot;model_null&quot;&gt;
&lt;option disabled ngxValue&gt;select something...&lt;/option&gt;
&lt;option [value]=&quot;1&quot;&gt;op1&lt;/option&gt;
&lt;/select&gt;
model_null: {{model_null|json}}
&lt;br&gt;
&lt;select [(ngModel)]=&quot;model_empty_string&quot;&gt;
&lt;option disabled ngxValue&gt;select something...&lt;/option&gt;
&lt;option [value]=&quot;1&quot;&gt;op1&lt;/option&gt;
&lt;/select&gt;
model_empty_string: {{model_empty_string|json}}
&lt;br&gt;
&lt;select [(ngModel)]=&quot;mode_with_value&quot;&gt;
&lt;option disabled ngxValue&gt;select something...&lt;/option&gt;
&lt;option [value]=&quot;1&quot;&gt;op1&lt;/option&gt;
&lt;/select&gt;
mode_with_value: {{mode_with_value|json}}
`,
})
export class App {
name = &#39;Angular&#39;;
model_undefined = undefined;
model_null = null;
model_empty_string = &#39;&#39;;
mode_with_value = &#39;1&#39;;
}

stackblitz

答案1

得分: 1

以下是翻译好的部分:

  1. 自定义控件("the angular way")
  2. 多个相互隐藏的选项用于空值("the quickest hack that works well and fast if you need it in a pinch")
  3. 结构型指令("the angular pros that write libraries way")

你可能需要一个自定义表单控件,作为一个习惯用法的解决方案。这里有一个工作示例,但主要思想是:

  1. 创建一个实现了Angular的ControlValueAccessor的自定义组件。
  2. 在组件内部,你只需像其他任何普通选择框一样拥有一个正常的选择框。但是你要通过表单API与外界通信。
  3. 在那里,你需要将所有的"空"值转换为自定义值 - 但在将该值传递回Angular时,你要找到实际的、隐藏的版本。

关键部分是自定义表单控件,类似于这样:

// 让我们有一个用于存储"空"值的东西。
const MY_EMPTY_VALUE = Symbol.for('my empty value.');

@Component({
  selector: 'my-select',
  standalone: true,
  template: `
  <select
  [ngModel]="myInternalValue"
  (ngModelChange)="onInternalChange($event)"
  [disabled]="isDisabled"
>
  <option [value]="MY_EMPTY_VALUE">{{ emptyValueLabel }}</option>
  <option *ngFor="let option of options" [value]="option.value">
    {{ option.label }}
  </option>
</select>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MySelectComponent),
      multi: true,
    },
  ],
  imports: [FormsModule, CommonModule],
})
export class MySelectComponent implements ControlValueAccessor {
  @Input() emptyValueLabel = 'Please select option';

  @Input() options: Array<{
    value: unknown;
    label: string;
  }> = [];

  // 这里组件的用户可以告诉我们他们认为是"空"的值。
  @Input() emptyValues: unknown[] = [null, undefined, '', false];

  // 我们需要这个实例上的属性,以便表单可以匹配。
  MY_EMPTY_VALUE = MY_EMPTY_VALUE.toString();

  // 我们将在内部保留的值有点像T,但我们还包括了我们独特的"空"符号。
  myInternalValue: unknown = null;

  isDisabled = false;

  onChange!: (value: unknown) => undefined;
  onTouched!: () => undefined;

  // Angular给了我们一个回调函数,用于告诉它我们认为我们的表单值已经改变了
  registerOnChange(fn: (value: unknown) => undefined): void {
    this.onChange = fn;
  }

  // Angular给了我们一个回调函数,用于告诉它我们认为我们的自定义控件被"触摸"了
  registerOnTouched(fn: () => undefined): void {
    this.onTouched = fn;
  }

  // 如果表单控件被禁用(或重新启用),Angular会调用这个方法。
  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

  // Angular告诉我们组件的"用户"写了一些值,例如通过编程方式设置ngModel。
  writeValue(value: unknown): void {
    console.log('Writing value from outside:', value);
    if (this.emptyValues.includes(value)) {
      this.myInternalValue = MY_EMPTY_VALUE.toString();
    } else {
      this.myInternalValue = value;
    }
  }

  onInternalChange(value: unknown) {
    console.log('Sending new value outside.');
    this.myInternalValue = value;
    this.onChange(value);
    this.onTouched();
  }
}

注意:我认为我没有正确地理解Symbol的独特性(因为toString的调用),但大致上应该是这个答案。

这是一个不错且快速的组件,它相对来说扩展性较好(意思是,你可以无问题地扩展它,直到一个点),而且它的工作效果也很好。


另一种选择是稍微巧妙一些,它能工作得更快,但不够可移植(你需要经常复制粘贴它):

<select [(ngModel)]="myModel">
  <option *ngIf="ngModel === undefined" [value]="undefined">Select value here</option>
  <option *ngIf="ngModel === null" [value]="null">Select value here</option>
  <option *ngIf="ngModel === ''" value="">Select value here</option>
  <option [value]="a">a</option>
  <option [value]="b">b</option>
</select>

这种方法之所以有效,是因为我们实际上有多个选项,并隐藏了不匹配的选项。这里是它的示例。

缺点是你可能会有重新绘制或者甚至闪烁的问题,这取决于你的样式、浏览器等等,但大多数情况下应该没问题。


如果你愿意,你可能可以将第二种方法变成一个自定义指令。不过,它必须是一个_结构型_指令。类似于ngIf或ngFor可以从无中生出东西的那种。这意味着你有一个空值的选项,但你还告诉它它是_结构性的_,这意味着它会提供其他模板值而不仅仅是你的值。

你可以这样使用它:

<select [(ngModel)]="myModel">
  <option *myEmptyOption="myModel">Select value here</option>
  <option *ngIf="ngModel === null" [value]="null">Select value here</option>
  <option *ngIf="ngModel === ''" value="">Select value here</option>
  <option [value]="

<details>
<summary>英文:</summary>

P.S. I&#39;m adding 3 possible ways to solve your issue (as I understand it) here :

1. Custom control (&quot;the angular way&quot;)
2. Multiple mutually hiding options for the empty values (&quot;the quickest hack that works well and fast if you need it in a pinch&quot;)
3. Structural directive (&quot;the angular pros that write libraries way&quot;)

---

You probably want a custom form control here, as a idiomatic solution. [Here](https://stackblitz.com/edit/stackblitz-starters-nndjfh)&#39;s a working example, but the main idea is this:

1. You create a custom component that implements Angular&#39;s `ControlValueAccessor`.
2. Within it, you just have a normal select like any other. But how you communicate to the outside world is via the Forms API.
3. There, you&#39;d convert all the &quot;empty&quot; values to something custom - but when giving that value back out to Angular, you find the actual, hidden version of the value.

The key part is the custom form control, something like this:

```ts
// Let us have something to store &quot;empty&quot; values.
const MY_EMPTY_VALUE = Symbol.for(&#39;my empty value.&#39;);

@Component({
  selector: &#39;my-select&#39;,
  standalone: true,
  template: `
  &lt;select
  [ngModel]=&quot;myInternalValue&quot;
  (ngModelChange)=&quot;onInternalChange($event)&quot;
  [disabled]=&quot;isDisabled&quot;
&gt;
  &lt;option [value]=&quot;MY_EMPTY_VALUE&quot;&gt;{{ emptyValueLabel }}&lt;/option&gt;
  &lt;option *ngFor=&quot;let option of options&quot; [value]=&quot;option.value&quot;&gt;
    {{ option.label }}
  &lt;/option&gt;
&lt;/select&gt;

  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() =&gt; MySelectComponent),
      multi: true,
    },
  ],
  imports: [FormsModule, CommonModule],
})
export class MySelectComponent implements ControlValueAccessor {
  @Input() emptyValueLabel = &#39;Please select option&#39;;

  @Input() options: Array&lt;{
    value: unknown;
    label: string;
  }&gt; = [];

  // Here the component users can tell us which values they consider &quot;empty&quot;.
  @Input() emptyValues: unknown[] = [null, undefined, &#39;&#39;, false];

  // We need this on the instance so that the form can match.
  MY_EMPTY_VALUE = MY_EMPTY_VALUE.toString();

  // The value we&#39;ll keep internally is kinda like T, but we also include our unique &quot;empty&quot; symbol.
  myInternalValue: unknown = null;

  isDisabled = false;

  onChange!: (value: unknown) =&gt; undefined;
  onTouched!: () =&gt; undefined;

  // Angular gives us a callback with which to tell it when we think our form value has changed
  registerOnChange(fn: (value: unknown) =&gt; undefined): void {
    this.onChange = fn;
  }

  // Angular gives us a callback with which to tell it when we think our custom control is &quot;touched&quot;
  registerOnTouched(fn: () =&gt; undefined): void {
    this.onTouched = fn;
  }

  // If the form control gets disabled (or enabled again), Angular calls this.
  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

  // Angular tells us the &quot;users&quot; of the component wrote some value, e.g. programmatically setting the ngModel.
  writeValue(value: unknown): void {
    console.log(&#39;Writing value from outside:&#39;, value);
    if (this.emptyValues.includes(value)) {
      this.myInternalValue = MY_EMPTY_VALUE.toString();
    } else {
      this.myInternalValue = value;
    }
  }

  onInternalChange(value: unknown) {
    console.log(&#39;Sending new value outside.&#39;);
    this.myInternalValue = value;
    this.onChange(value);
    this.onTouched();
  }
}

Note: I think I didn't get the Symbol uniqueness magic correctly (cause of that toString call) but approximately this would be the answer.

This is a nice and quick component, it scales relatively well (meaning, you can extend it with no issues, up to a point) and it does the job well.


Another alternative would be to be a little hacky - it's working and it's faster, but it's not as portable (you'd have to copy-paste it frequently):

&lt;select [(ngModel)]=&quot;myModel&quot;&gt;
  &lt;option *ngIf=&quot;ngModel === undefined&quot; [value]=&quot;undefined&quot;&gt;Select value here&lt;/option&gt;
  &lt;option *ngIf=&quot;ngModel === null&quot; [value]=&quot;null&quot;&gt;Select value here&lt;/option&gt;
  &lt;option *ngIf=&quot;ngModel === &#39;&#39;&quot; value=&quot;&quot;&gt;Select value here&lt;/option&gt;
  &lt;option [value]=&quot;a&quot;&gt;a&lt;/option&gt;
  &lt;option [value]=&quot;b&quot;&gt;b&lt;/option&gt;
&lt;/select&gt;

The reason this works is that we're actually having multiple options, and hiding the non-metching ones. Here's that version in action.

The downside is that you potentially have repaints or even flicker, depending on your styling, browsers etc. most likely it's gonna be fine though.


If you wanted, you could probably make this second version into a custom directive. Except, it'd have to be a structural directive. The kind that customly creates and destroys elements. Kinda like how *ngIf or *ngFor can create things out of thin air. Means, you have an option for an empty value, but you also tell it it's structural which means it will provide other template values instead of just yours.

You would use it like this:


&lt;select [(ngModel)]=&quot;myModel&quot;&gt;
  &lt;option *myEmptyOption=&quot;myModel&quot;&gt;Select value here&lt;/option&gt;
  &lt;option *ngIf=&quot;ngModel === null&quot; [value]=&quot;null&quot;&gt;Select value here&lt;/option&gt;
  &lt;option *ngIf=&quot;ngModel === &#39;&#39;&quot; value=&quot;&quot;&gt;Select value here&lt;/option&gt;
  &lt;option [value]=&quot;a&quot;&gt;a&lt;/option&gt;
  &lt;option [value]=&quot;b&quot;&gt;b&lt;/option&gt;
&lt;/select&gt;

The critical part of the directive itself would look like this:

@Input() set emptyOptionValue(value: unknown) {
if (value === null || value === undefined || value === &#39;&#39;) {
// you&#39;d make your empty option selected here
} else {
// you&#39;d make your empty option deselected here
}
}

I don't have a stackblitz for this one, but I hope you can get there on your own following on the examples from above.

答案2

得分: 0

"disabled hidden"的作用是使得在下拉列表中不能选择(在下拉列表中不会显示选项),但如果值为null、undefined或'',则会显示"select something"。

英文:

why not use?

 &lt;option disabled hidden *ngFor=&quot;let option of [null,undefined,&#39;&#39;]&quot; 
[ngValue]=&quot;option&quot;&gt;select something...&lt;/option&gt;

The "disabled hidden" makes that you can not selected (dont' appear in the options when you drop down) but show the "select something" if the value is null, undefined or ''

huangapple
  • 本文由 发表于 2023年7月7日 00:52:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/76631003.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定