简体   繁体   中英

Aurelia binding - back to view model on button click

In Aurelia application I have a "Rename" form with a single input

<input ... value.bind="something.name" />

and two buttons: Save and Cancel .

The same something object is already used in other controls. So, I don't want the name to change until Save button is clicked.

Is there a nice declarative way to accomplish that or do I have to copy name to another property and then copy it back on Save trigger?

You can create a model object for your edit popup and only copy the edits made to the item in the list when Save is clicked. Here's a simplified example: https://gist.run/?id=af3af031c5acc4c46407679f5ab1376b

View

<template>
  <ul>
    <li repeat.for="person of people">${person.firstName} ${person.lastName} <button click.delegate="editPerson(person)">Edit</button></li>
  </ul>
  <div if.bind="editing">
    First Name: <input type="name" value.bind="editModel.firstName" />
    Last Name: <input type="name" value.bind="editModel.lastName" />
    <button click.delegate="savePerson()">Save</button>
    <button click.delegate="cancelEdit()">Cancel</button>
  </div>
</template>

ViewModel

export class App {
  editing = false;
  people = [
    { firstName: 'John', lastName: 'Doe' },
    { firstName: 'Jane', lastName: 'Smith' },
    { firstName: 'Bob', lastName: 'Smith' }
  ];

    editPerson(person) {
      this.editing = true;
      this.editObject = person;
      this.editModel = Object.assign({},person);
    }
    savePerson() {
      this.editing = false;

      Object.assign(this.editObject, this.editModel);

      this.editObject = null;
      this.editModel = null;
    }

    cancelEdit() {
      this.personBeingEdited = null;
      this.editing = false;
    }
}

I think Ashley Grant's answer is a very clear/straightforward way of doing this. I would go with something like that. Since you asked for an alternate approach, that uses less code, here you go.... not sure if it's any better, it's just another way of doing it, probably less clear...

https://gist.run?id=e931202307361d472c3e0ee4f523a833

The view model has a property called editPerson which represents the person that is currently being edited.

app.js

export class App {
  people = [
    { firstName: 'John', lastName: 'Doe' },
    { firstName: 'Jane', lastName: 'Smith' },
    { firstName: 'Bob', lastName: 'Smith' }
  ];

  editPerson = null;

  save() {
    this.editPerson.firstName = this.firstNameInput.value;
    this.editPerson.lastName = this.lastNameInput.value;
    this.editPerson = null;
  }
}

The view uses one-way bindings to push the view-model data into the inputs. Edits to the inputs will not update the model because the binding is one way. When the form is submitted the view-model's save() method will be called, which has logic to copy the input values into the model.

app.html

<template>
  <ul>
    <li repeat.for="person of people">
      ${person.firstName} ${person.lastName}
      <button click.delegate="editPerson = person">Edit</button>
    </li>
  </ul>

  <form if.bind="editPerson" submit.delegate="save()">
    <label>
      First Name:
      <input ref="firstNameInput" value.one-way="editPerson.firstName">
    </label>
    <label>
      Last Name:
      <input ref="lastNameInput" value.one-way="editPerson.lastName">
    </label>

    <button type="submit">Save</button>
    <button type="button" click.delegate="editPerson = null">Cancel</button>
  </form>
</template>

Here is a alternative using a custom "cancellable" binding behavior. The binding would do these things:

  1. Intercept updateSource to redirect writing to a hidden storage
  2. Listen for save event to write back the hidden storage to original source.

"2" is done by convention that observe a saved property on the binding context. If need more flexibility, the property name can be passed as an argument to the custom binding.

app.html

<template>
  <require from='./cancellable'></require>

  <div>
    Name: ${name}
    Age: ${age}
    <button click.delegate="edit()">Edit</button>
  </div>

  <div if.bind="editing">
    <h3>Cancellable edit</h3>
    Name: <input value.bind="name & cancellable">
    Age: <input value.bind="age & cancellable">
    <div><button click.delegate="save()">Save</button>
    <button click.delegate="cancel()">Cancel</button></div>
  </div>

  <div if.bind="editing">
    <h3>Instant edit</h3>
    Name: <input value.bind="name">
    Age: <input value.bind="age">
  </div>
</template>

app.js

export class App {
  constructor() {
    this.name = 'John';
    this.age = 20;

    this.editing = false;
    this.saved = false;
  }


  edit() {
    this.saved = false;
    this.editing = true;
  }

  save() {
    this.saved = true;
    this.editing = false;
  }

  cancel() {
    this.saved = false;
    this.editing = false;
  }
}

cancellable.js

import {inject} from 'aurelia-dependency-injection';
import {BindingEngine} from 'aurelia-binding';

@inject(BindingEngine)
export class CancellableBindingBehavior {
  constructor(bindingEngine) {
    this.bindingEngine = bindingEngine;
  }

  bind(binding, scope) {
    let value;    
    let modified = false;

    let cancellable = {
      originalUpdateSource: binding.updateSource,
      originalUpdateTarget: binding.updateTarget,
    };

    // 1. Intercept "updateSource" to redirect write to a hidden value storage
    binding.updateSource = (val) => {
      value = val;
      modified = true;
    };

    // 2. Intercept updateTarget" so that can observe change from original source 
    binding.updateTarget = (val) => {
      value = val;
      modified = false;
      cancellable.originalUpdateTarget.call(binding, val);
    }

    // 3. Observe the "saved" event to copy back to original source 
    let bindingContext = scope.bindingContext;
    cancellable.subscription = this.bindingEngine.propertyObserver(bindingContext, 'saved')
      .subscribe((newValue, oldValue) => {
        if (newValue && modified) {
          cancellable.originalUpdateSource.call(binding, value);
        }
      });

    binding.cancellable = cancellable;
  }

  unbind(binding, scope) {
    binding.updateSource = binding.cancellable.originalUpdateSource;
    binding.updateTarget = binding.cancellable.originalUpdateTarget;
    binding.cancellable.subscription.dispose();
    binding.cancellable = null;
  }
}

Gist run: https://gist.run/?id=2c7e40e88d1d3c18e9d2bca6be438b47

Reference: the built-in throttle binding behavior

What I've done is made a copy of the object I want to edit and then bind that to the view, when I click save I then 'merge' the editedObject with my originalObject so the changes are reflected in the other controls.

So my edit ViewModel looks like this:

activate(existingUser: User) {
    let userCopy: User = JSON.parse(JSON.stringify(existingUser));
}

In my view I then user the userCopy object to bind properties. When I click save I do the following:

let indexOfUser = this.users.indexOf(existingUser);
Object.assign(this.users[indexOfUser], userCopy);

I've simplified the code a bit because in my use case I'm working with a dialog that returns the editedUser to a different ViewModel and that's the ViewModel that holds the list of users etc.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM