In our previous article, we created an account (hopefully, you did too). In this article, we'll be implementing contact creation, deletion and editing.

At the heart of event sourcing are aggregates, commands and events. An aggregate is simply a domain object e.g. BankAccount, Order, Customer etc. A command is an intention to do something e.g. CreateCustomerAccount, DebitBankAccount, CreateOrder etc. An event is the result of successfully processing a command eg CustomerAccountCreated,BankAccountDebited, OrderCreated etc. In summary, commands are applied on aggregates resulting in events.

Our contacts will have only name and gender attributes.

Let's start by creating a ContactAggregate. Create a file app/Domain/Contact/ContactAggregate.php with the following contents:

<?php

namespace App\Domain\Contact;

use Spatie\EventProjector\AggregateRoot;

final class ContactAggregate extends AggregateRoot {
}

Let's add a createContact method to create ContactCreated events and anapplyContactCreated method to update the ContactAggregate state. Our ContactAggregate now looks like this:

<?php

namespace App\Domain\Contact;

use Spatie\EventProjector\AggregateRoot;
use App\Domain\Contact\Events\ContactCreated;

final class ContactAggregate extends AggregateRoot {
  /** @var string */
  public $name;

  /** @var string */
  public $gender;

  /** @var string */
  public $userId;

  public function createContact(string $userId, string $name, string $gender) {
    $this->recordThat(new ContactCreated($userId, $name, $gender));

    $this->persist();
  }

  public function applyContactCreated(ContactCreated $event) {
    $this->name = $event->name;
    $this->gender = $event->gender;
    $this->userId = $event->userId;

    $this->persist();
  }
}

The persist() call is to save the aggregate and all recorded events (the ones in the recordThat calls) to the database and call the required projectors and reactors (think of them as event handlers).

Let's create the ContactCreated event in app/Domain/Contact/Events/ContactCreated.php with the following:

<?php

namespace App\Domain\Contact\Events;
use Spatie\EventProjector\ShouldBeStored;

final class ContactCreated implements ShouldBeStored {
  /** @var string */
  public $name;

  /** @var string */
  public $gender;

  /** @var string */
  public $userId;

  public function __construct(string $userId, string $name, string $gender) {
    $this->userId = $userId;
    $this->name = $name;
    $this->gender = $gender;
  }
}

You might be asking: now that we found an event, what are we gonna do with it? I'm glad you asked (even if you didn't). This is a perfect time to introduce projectors.

A projector is basically an event handler that takes created events and does some CRUD operations on our read models with the contents of the events. While we're here, a reactor is a projector that carries out some job instead of fiddling with databases (e.g. send a welcome mail).

We'll create a projector to listen for ContactCreated events and create contacts in our database.

Run php artisan make:migration create_contacts_table and paste the following to the migration file:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateContactsTable extends Migration {
  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up() {
    Schema::create('contacts', function (Blueprint $table) {
      $table->uuid('id')->primary();
      $table->uuid('user_id');
      $table->string('name');
      $table->string('gender');
      $table->timestamps();
    });
  }

  /**
   * Reverse the migrations.
   *
   * @return void
   */
  public function down() {
    Schema::dropIfExists('contacts');
  }
}
You might have noticed we are not creating any foreign keys. In event sourcing, all business rules and constraints are enforced in commands before events are created. Our read models are basically dumb stores.

Next, create a Contact.php model in app/Models with these contents:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Contact extends Model {
  use Traits\UsesUuid;

  protected $guarded = [];

  public $incrementing = false;

  public function user() {
    return $this->belongsTo(User::class);
  }
}

With that out of the way, create a projector app/Domain/Contact/Projectors/ContactProjector.php with the contents below:

<?php

namespace App\Domain\Contact\Projectors;

use Spatie\EventProjector\Projectors\Projector;
use Spatie\EventProjector\Projectors\ProjectsEvents;
use App\Domain\Contact\Events\ContactCreated;
use App\Models\Contact;

class ContactProjector implements Projector {
  use ProjectsEvents;

  public function onContactCreated(ContactCreated $event, string $aggregateUuid) {
    Contact::create([
      'id' => $aggregateUuid,
      'gender' => $event->gender,
      'name' => $event->name,
      'user_id' => $event->userId
    ]);
  }
}

To create a contact, we'll have to issue a command. Let's create a command app/Domain/Contact/Commands/CreateContact.php with the following contents:

<?php

namespace App\Domain\Contact\Commands;

use Ramsey\Uuid\Uuid;
use App\Models\User;
use App\Models\Contact;
use App\Domain\Command;
use App\Domain\Contact\ContactAggregate;

final class CreateContact extends Command {
  public function __construct(array $bag) {
    $this->attributes['user_id'] = Uuid::isValid($bag['user_id']) ?
      $bag['user_id'] :
      Uuid::fromString($bag['user_id']);

    $this->attributes['name'] = $bag['name'];
    $this->attributes['gender'] = strtolower($bag['gender']);
  }

  public static function from(array $bag) : CreateContact {
    return new CreateContact($bag);
  }

  public function isValid() : bool {
    return ((bool) User::find($this->attributes['user_id'])) &&
      ! empty($this->attributes['name']) &&
      in_array($this->attributes['gender'], ['male', 'female']);
  }

  public function execute() : ?Contact {
    if (! $this->isValid()) return null;

    $newId = Uuid::uuid4();

    ContactAggregate::retrieve($newId)
      ->createContact($this->attributes['user_id'], $this->attributes['name'], $this->attributes['gender']);

    return Contact::find($newId);
  }
}

It's a good idea to put our common command properties in a parent class app/Domain/Command.php:

<?php

namespace App\Domain;

abstract class Command {
  /** @var array */
  protected $attributes;

  abstract public function isValid() : bool;

  abstract public function execute();

  public function getAttributes() : array {
    return $this->attributes;
  }
}

At this point we have wired everything up to make contact creation possible. But, how do we know it works, you might ask (even if you didn't ask)? We'll create tests.

Let's modify tests/TestCase.php for our purposes:

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Faker\Factory as Faker;
use Illuminate\Support\Str;
use App\Models\User;

abstract class TestCase extends BaseTestCase {
    use CreatesApplication;

    private $faker;

    protected function getUserAttributes() {
      if (empty($this->faker)) $this->faker = Faker::create();

      return [
        'id' => Str::uuid(),
        'email' => $this->faker->safeEmail,
        'name' => $this->faker->name,
        'password' => $this->faker->password
      ];
    }

    protected function getContactAttributes(User $user) {
      if (empty($this->faker)) $this->faker = Faker::create();

      return [
        'user_id' => $user->id,
        'name' => $this->faker->name,
        'gender' => rand() % 2 == 0 ? 'male' : 'female'
      ];
    }
}

Rename tests/Feature/ExampleTest.php to tests/Feature/ContactTest.php and paste the following code into it:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Domain\Contact\Commands\CreateContact;
use App\Models\User;
use App\Models\Contact;

class ContactTest extends TestCase {
  use RefreshDatabase;

  public function testContactCreation() {
    $user = User::create($this->getUserAttributes());
    $data = $this->getContactAttributes($user);
    $createContactCommand = CreateContact::from($data);

    $this->assertEquals(count($createContactCommand->getAttributes()), count($data));
    $this->assertTrue($createContactCommand->isValid());

    $contact = $createContactCommand->execute();

    $this->assertTrue($contact instanceof Contact);
    $this->assertEquals($contact->name, $data['name']);
    $this->assertEquals($contact->gender, $data['gender']);
    $this->assertEquals($contact->user_id, $user->id);
  }
}

If you run vendor/bin/phpunit, our test should pass (don't blame me if yours does not).

Let's move over to building the front-end for this feature.

Add the following code to the User model to help us fetch a user's contacts:

public function contacts() {
    return $this->hasMany(Contact::class);
}

Update the contacts() route handler of HomeController to resemble this:

public function contacts() {
    return view('contacts', ['contacts' => \Auth::user()->contacts()->get()]);
}

Update resources/views/contacts.blade.php to:

@section('content')
<div class="container-fluid">
  <contact-view :contacts="{{ $contacts }}" />
</div>
@endsection

Change the container class of the nav element in resources/views/layouts/app.blade.php to container-fluid.

Create app/Http/Controllers/ContactController.php and paste the following:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Auth;
use App\Models\Contact;
use App\Domain\Contact\Commands\CreateContact;

class ContactController extends Controller {
  public function store(Request $request) {
    $data = $request->all();
    $data['user_id'] = Auth::guard('api')->user()->id;
    $command = CreateContact::from($data);

    if (! $command->isValid()) {
      return response([
        'success' => false,
        'message' => 'Invalid params'
      ], Response::HTTP_UNPROCESSABLE_ENTITY);
    }

    $contact = $command->execute();

    return response([
      'success' => true,
      'message' => 'Created',
      'data' => $contact->jsonSerialize()
    ], Response::HTTP_CREATED);
  }
}

Replace routes/api.php with:

<?php

use Illuminate\Http\Request;

Route::group(['middleware' => ['auth:api']], function() {
  Route::resource('contacts', 'ContactController', ['only' => ['store', 'update', 'destroy']]);
});

Replace resources/js/app.js with:

/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

window.Vue = require('vue');

Vue.component('contact-view', require('./components/ContactView.vue').default);

new Vue({ el: '#app' });

In resources/js/components, create 3 files ContactView.vue, ContactList.vue and CreateContact.vue.

ContactView.vue's contents are:

<template>
  <div class="row justify-content-center">
    <div class="col-md-4">
      <create-contact-form @contact-created="handleContactCreated" />
      <contact-list :contacts="localContacts" />
    </div>
    <div class="col-md-8">
      <div class="card">
        <div class="card-body">
          <!-- View contact -->
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import CreateContact from './CreateContact';
import ContactList from './ContactList';

export default {
  name: 'ContactView',
  components: {
    CreateContact,
    ContactList
  },
  props: {
    contacts: {
      type: Array,
      default: []
    }
  },
  data() {
    return {
      localContacts: this.contacts
    };
  },
  methods: {
    handleContactCreated(contact) {
      this.localContacts.push(contact);
    }
  }
};
</script>

ContactList.vue's contents are:

<template>
  <div class="card">
    <div class="card-body">
      <ul class="list-group">
        <li class="list-group-item" v-for="contact in contacts" :key="contact.id">
          <div class="row">
            <div class="col-md-9">
              <h5>{{ contact.name }}</h5>
              <small>{{ contact.gender }}</small>
            </div>
            <div class="col-md-3 text-right">
              <button class="btn btn-sm btn-outline-secondary">View</button>
            </div>
          </div>
        </li>
      </ul>
      <h4 v-if="!contacts.length" class="text-muted text-center">No contacts</h4>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ContactList',
  props: {
    contacts: {
      type: Array,
      required: true
    }
  }
}
</script>

CreateContact.vue's contents are:

<template>
  <div class="card mb-4">
    <div class="card-body">
      <div class="form-group">
        <label>Name</label>
        <input type="text" v-model="form.name" class="form-control">
      </div>
      <div class="form-group">
        <label>Gender</label>
        <select v-model="form.gender" class="form-control">
          <option value="male">Male</option>
          <option value="female">Female</option>
        </select>
      </div>
      <div class="form-group">
        <button :disabled="!formOk" @click.stop="submit()">Create contact</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CreateContact',
  data() {
    return {
      form: {
        name: '',
        gender: 'male'
      }
    };
  },
  computed: {
    formOk() {
      return !!this.form.name && ['male', 'female'].indexOf(this.form.gender) !== -1;
    }
  },
  methods: {
    submit() {
      axios.post('/api/contacts', this.form)
        .then(({ data: { data } }) => {
          this.$emit('contact-created', data);

          this.form.name = '';
        })
        .catch(({ response }) => {
          alert('Could not create');
        });
    }
  }
}
</script>

If you don't have npm run watch running, do so now to compile the Vue files.

Sign in and create contacts to your heart's contents while I stretch out a little...

Ok, I'm back. You should have created a lot of contacts by now and would want to delete some. Let's implement contact deletion.

Create app/Domain/Contact/Events/ContactDeleted.php and paste the following:

<?php

namespace App\Domain\Contact\Events;
use Spatie\EventProjector\ShouldBeStored;

final class ContactDeleted implements ShouldBeStored {
  /** @var string */
  public $contactId;

  public function __construct(string $contactId) {
    $this->contactId = $contactId;
  }
}

Then add the following method to the ContactAggregate (remember to add the use statement for ContactDeleted):

public function deleteContact(string $id) {
    $this->recordThat(new ContactDeleted($id));

    $this->persist();
}

Add the following method (and the accompanying use statement for ContactDeleted) to ContactProjector:

public function onContactDeleted(ContactDeleted $event, string $aggregateUuid) {
    Contact::where('id', $aggregateUuid)->delete();
}

Create app/Domain/Contact/Commands/DeleteContact.php and paste:

<?php

namespace App\Domain\Contact\Commands;

use Ramsey\Uuid\Uuid;
use App\Models\Contact;
use App\Domain\Command;
use App\Domain\Contact\ContactAggregate;

final class DeleteContact extends Command {
  public function __construct(array $bag) {
    $this->attributes['id'] = Uuid::isValid($bag['id']) ? $bag['id'] : Uuid::fromString($bag['id']);
  }

  public static function from(array $bag) : DeleteContact {
    return new DeleteContact($bag);
  }

  public function isValid() : bool {
    return (bool) Contact::find($this->attributes['id']);
  }

  public function execute() {
    if (! $this->isValid()) return;

    ContactAggregate::retrieve($this->attributes['id'])->deleteContact($this->attributes['id']);
  }
}

Add the following test to your ContactTest class to test contact deletion:

public function testContactDeletion() {
    $user = User::create($this->getUserAttributes());
    $contact = CreateContact::from($this->getContactAttributes($user))->execute();

    $this->assertEquals(1, count($user->contacts()->get()));

    $command = DeleteContact::from(['id' => $contact->id]);

    $this->assertTrue($command->isValid());

    $command->execute();

    $this->assertEquals(0, count($user->contacts()->get()));
  }

The test should pass, so let's add the front-end.

First, add the route handler for deletion to the ContactController (remember the use for DeleteContact):

public function destroy($id) {
    $contact = Auth::guard('api')->user()->contacts()->where('id', $id)->first();

    if ($contact) {
      DeleteContact::from(['id' => $contact->id])->execute();

      return response(null, Response::HTTP_NO_CONTENT);
    }

    return response(null, Response::HTTP_FORBIDDEN);
}

Then, update the ContactList.vue component to:

<template>
  <div class="card">
    <div class="card-body">
      <ul class="list-group">
        <li class="list-group-item" v-for="contact in contacts" :key="contact.id">
          <div class="row">
            <div class="col-md-9">
              <h5>{{ contact.name }}</h5>
            </div>
            <div class="col-md-3 text-right">
              <div class="btn-group">
                <button class="btn btn-sm btn-outline-danger" @click="deleteContact(contact.id)">Delete</button>
                <button class="btn btn-sm btn-outline-secondary">View</button>
              </div>
            </div>
          </div>
        </li>
      </ul>
      <h4 v-if="!contacts.length" class="text-muted text-center">No contacts</h4>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ContactList',
  props: {
    contacts: {
      type: Array,
      required: true
    }
  },
  methods: {
    deleteContact(id) {
      if (! confirm('Are you sure?')) return;

      axios.delete(`/api/contacts/${id}`)
        .then(() => {
          this.$emit('contact-deleted', id);
        })
        .catch(({ response }) => {
          alert('Could not delete');
        });
    }
  }
}
</script>

And the ContactView.vue component to:

<template>
  <div class="row justify-content-center">
    <div class="col-md-4">
      <create-contact @contact-created="handleContactCreated" />
      <contact-list @contact-deleted="handleContactDeleted" :contacts="localContacts" />
    </div>
    <div class="col-md-8">
      <div class="card">
        <div class="card-body">
          <!-- View contact -->
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import CreateContact from './CreateContact';
import ContactList from './ContactList';

export default {
  name: 'ContactView',
  components: {
    CreateContact,
    ContactList
  },
  props: {
    contacts: {
      type: Array,
      default: []
    }
  },
  data() {
    return {
      localContacts: this.contacts
    };
  },
  methods: {
    handleContactCreated(contact) {
      this.localContacts.push(contact);
    },
    handleContactDeleted(id) {
      this.localContacts = this.localContacts.filter(c => c.id !== id);
    }
  }
};
</script>

That's all for deleting contacts.

To finish off our contacts feature, let's implement support for updating contacts.

For creation and deletion, our commands and events have had a one-to-one mapping. For updates, we'll see an example of creating more than one event from a command.

Each attribute of the contact is updated independently. So let's create the two events  ContactNameChanged and ContactGenderChanged respectively:

<?php

namespace App\Domain\Contact\Events;
use Spatie\EventProjector\ShouldBeStored;

final class ContactNameChanged implements ShouldBeStored {
  /** @var string */
  public $name;

  /** @var string */
  public $contactId;

  public function __construct(string $contactId, string $name) {
    $this->contactId = $contactId;
    $this->name = $name;
  }
}
<?php

namespace App\Domain\Contact\Events;
use Spatie\EventProjector\ShouldBeStored;

final class ContactGenderChanged implements ShouldBeStored {
  /** @var string */
  public $gender;

  /** @var string */
  public $contactId;

  public function __construct(string $contactId, string $gender) {
    $this->contactId = $contactId;
    $this->gender = $gender;
  }
}

Add the following handler methods to the ContactProjector:

public function onContactNameChanged(ContactNameChanged $event, string $aggregateUuid) {
    Contact::where('id', $aggregateUuid)->update([
      'name' => $event->name
    ]);
}

public function onContactGenderChanged(ContactGenderChanged $event, string $aggregateUuid) {
    Contact::where('id', $aggregateUuid)->update([
      'gender' => $event->gender
    ]);
}

We'll need to update our ContactAggregate with these:

public function updateContact(string $contactId, string $name, string $gender) {
    $updated = false;

    if (! empty($name) && $name != $this->name) {
      $this->recordThat(new ContactNameChanged($contactId, $name));

      $updated = true;
    }

    if (! empty($gender) && $gender != $this->gender) {
      $this->recordThat(new ContactGenderChanged($contactId, $gender));

      $updated = true;
    }

    if ($updated) $this->persist();
}

public function applyContactNameChanged(ContactNameChanged $event) {
    $this->name = $event->name;

    $this->persist();
}

public function applyContactGenderChanged(ContactGenderChanged $event) {
    $this->gender = $event->gender;

    $this->persist();
}

Create a new command in the Contact domain called UpdateContact and paste these in:

<?php

namespace App\Domain\Contact\Commands;

use Ramsey\Uuid\Uuid;
use App\Models\User;
use App\Models\Contact;
use App\Domain\Command;
use App\Domain\Contact\ContactAggregate;

final class UpdateContact extends Command {
  public function __construct(array $bag) {
    $this->attributes['contact_id'] = Uuid::isValid($bag['contact_id']) ?
      $bag['contact_id'] :
      Uuid::fromString($bag['contact_id']);

    if (! empty($bag['name'])) $this->attributes['name'] = $bag['name'];
    if (! empty($bag['gender'])) $this->attributes['gender'] = strtolower($bag['gender']);
  }

  public static function from(array $bag) : UpdateContact {
    return new UpdateContact($bag);
  }

  public function isValid() : bool {
    $contactExists = ((bool) Contact::find($this->attributes['contact_id']));
    $nameOk = ! empty($this->attributes['name']) || true; // Ignore if not provided
    $genderOk = ! empty($this->attributes['gender']) ?
      in_array($this->attributes['gender'], ['male', 'female']) : // If provided, check validity
      true; // Ignore if not provided

    return $contactExists && $nameOk && $genderOk;
  }

  public function execute() : ?Contact {
    if (! $this->isValid()) return null;

    ContactAggregate::retrieve($this->attributes['contact_id'])
      ->updateContact(
        $this->attributes['contact_id'],
        ! empty($this->attributes['name']) ? $this->attributes['name'] : '',
        ! empty($this->attributes['gender']) ? $this->attributes['gender'] : ''
      );

    return Contact::find($this->attributes['contact_id']);
  }
}

We then add some tests to our ContactTest to ensure our additions work as expected:

public function testContactNameUpdate() {
    $user = User::create($this->getUserAttributes());
    $contact = CreateContact::from($this->getContactAttributes($user))->execute();
    $data = ['contact_id' => $contact->id];
    $data['name'] = $this->getContactAttributes($user)['name'];
    $command = UpdateContact::from($data);

    $this->assertTrue($command->isValid());

    $updatedContact = $command->execute();

    $this->assertNotEquals($updatedContact->name, $contact->name);
    $this->assertEquals($updatedContact->name, $data['name']);
    $this->assertEquals($updatedContact->gender, $contact->gender);
  }

  public function testContactGenderUpdate() {
    $user = User::create($this->getUserAttributes());
    $contact = CreateContact::from($this->getContactAttributes($user))->execute();
    $data = ['contact_id' => $contact->id];
    $genderParam = $this->getContactAttributes($user)['gender'];

    // Since we're randomly choosing genders, we want to continue choosing till we get a gender that's different from that of the contact
    while ($genderParam == $contact->gender) $genderParam = $this->getContactAttributes($user)['gender'];

    $data['gender'] = $genderParam;
    $command = UpdateContact::from($data);

    $this->assertTrue($command->isValid());

    $updatedContact = $command->execute();

    $this->assertNotEquals($updatedContact->gender, $contact->gender);
    $this->assertEquals($updatedContact->gender, $data['gender']);
    $this->assertEquals($updatedContact->name, $contact->name);
  }

  public function testContactUpdateWithUnchangedData() {
    $user = User::create($this->getUserAttributes());
    $contact = CreateContact::from($this->getContactAttributes($user))->execute();
    $data = ['contact_id' => $contact->id, 'name' => $contact->name, 'gender' => $contact->gender];

    $command = UpdateContact::from($data);

    $this->assertTrue($command->isValid());

    $updatedContact = $command->execute();

    $this->assertEquals($updatedContact->updated_at, $contact->updated_at);
  }

For the front-end, add the following route handler to ContactController:

public function update(Request $request, $id) {
    $contact = Auth::guard('api')->user()->contacts()->where('id', $id)->first();

    if ($contact) {
      $contact = UpdateContact::from(array_merge($request->all(), ['contact_id' => $id]))->execute();

      return response([
        'success' => (bool) $contact,
        'data' => $contact,
        'message' => ((bool) $contact) ? 'Updated' : 'Not updated'
      ], ((bool) $contact) ? Response::HTTP_OK : Response::HTTP_BAD_REQUEST);
    }

    return response(null, Response::HTTP_FORBIDDEN);
  }

Since we'll also be using a form identical to the one we created in CreateContact.vue, let's extract the form into its own component called ContactForm.vue:

<template>
  <form @submit.stop.prevent="submit()">
    <div class="form-group">
      <label>Name</label>
      <input type="text" v-model="form.name" class="form-control">
    </div>
    <div class="form-group">
      <label>Gender</label>
      <select v-model="form.gender" class="form-control">
        <option value="male">Male</option>
        <option value="female">Female</option>
      </select>
    </div>
    <div class="form-group">
      <button :disabled="!formOk" type="submit">{{ buttonText }}</button>
    </div>
  </form>
</template>

<script>
export default {
  name: 'ContactForm',
  props: {
    initialData: {
      type: Object,
      required: true
    },
    buttonText: {
      type: String,
      required: true
    }
  },
  data() {
    const form = this.initialData ?
      { name: this.initialData.name, gender: this.initialData.gender } :
      { name: '', gender: '' };

    return {
      form
    };
  },
  computed: {
    formOk() {
      return !!this.form.name && ['male', 'female'].indexOf(this.form.gender) !== -1;
    }
  },
  methods: {
    submit() {
      this.$emit('form-submitted', this.form);
    },
    resetForm({ name, gender }) {
      this.form.name = name;
      this.form.gender = gender;
    }
  }
}
</script>

Our CreateContact.vue component now becomes:

<template>
  <div class="card mb-4">
    <div class="card-body">
      <contact-form
        ref="form"
        button-text="Create contact"
        :initial-data="form"
        @form-submitted="submit" />
    </div>
  </div>
</template>

<script>
import ContactForm from './ContactForm';

export default {
  name: 'CreateContact',
  components: {
    ContactForm
  },
  data() {
    return {
      form: {
        name: '',
        gender: 'male'
      }
    };
  },
  methods: {
    submit(form) {
      axios.post('/api/contacts', form)
        .then(({ data: { data } }) => {
          this.$emit('contact-created', data);

          this.$refs.form.resetForm(this.form);
        })
        .catch(({ response }) => {
          alert('Could not create');
        });
    }
  }
}
</script>

Update ContactView.vue to:

<template>
  <div class="row justify-content-center">
    <div class="col-md-4">
      <create-contact @contact-created="handleContactCreated" />
      <contact-list
        @contact-deleted="handleContactDeleted"
        @contact-selected="handleContactSelected"
        :contacts="localContacts" />
    </div>
    <div class="col-md-8">
      <contact :contact="selectedContact" @contact-updated="handleContactUpdated" />
    </div>
  </div>
</template>

<script>
import CreateContact from './CreateContact';
import ContactList from './ContactList';
import Contact from './Contact';

export default {
  name: 'ContactView',
  components: {
    CreateContact,
    ContactList,
    Contact
  },
  props: {
    contacts: {
      type: Array,
      default: []
    }
  },
  data() {
    return {
      localContacts: this.contacts,
      selectedContact: null
    };
  },
  methods: {
    handleContactCreated(contact) {
      this.localContacts.push(contact);
    },
    handleContactDeleted(id) {
      this.localContacts = this.localContacts.filter(c => c.id !== id);
    },
    handleContactUpdated(contact) {
      this.localContacts = this.localContacts.map(c => c.id === contact.id ? contact : c);

      if ((this.selectedContact || { id: '' }).id === contact.id) {
        this.selectedContact = contact;
      }
    },
    handleContactSelected(id) {
      this.selectedContact = this.localContacts.find(c => c.id === id);
    }
  }
};
</script>

Update ContactList.vue to:

<template>
  <div class="card">
    <div class="card-body">
      <ul class="list-group">
        <li class="list-group-item" v-for="contact in contacts" :key="contact.id">
          <div class="row">
            <div class="col-md-9">
              <h5>{{ contact.name }}</h5>
              <small>{{ contact.gender }}</small>
            </div>
            <div class="col-md-3 text-right">
              <div class="btn-group">
                <button class="btn btn-sm btn-outline-danger" @click="deleteContact(contact.id)">Delete</button>
                <button class="btn btn-sm btn-outline-secondary" @click="selectContact(contact.id)">View</button>
              </div>
            </div>
          </div>
        </li>
      </ul>
      <h4 v-if="!contacts.length" class="text-muted text-center">No contacts</h4>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ContactList',
  props: {
    contacts: {
      type: Array,
      required: true
    }
  },
  methods: {
    deleteContact(id) {
      if (! confirm('Are you sure?')) return;

      axios.delete(`/api/contacts/${id}`)
        .then(() => {
          this.$emit('contact-deleted', id);
        })
        .catch(({ response }) => {
          alert('Could not delete');
        });
    },
    selectContact(id) {
      this.$emit('contact-selected', id);
    }
  }
}
</script>

Create a new component called Contact.vue with the following contents:

<template>
  <div class="card">
    <div class="card-body">
      <h3 class="text-muted text-center" v-if="!contact">No contact selected</h3>
      <div v-else>
        <contact-form
          ref="form"
          button-text="Update contact"
          :initial-data="form"
          @form-submitted="submit" />
        <hr>
      </div>
    </div>
  </div>
</template>

<script>
import ContactForm from './ContactForm';

export default {
  name: 'Contact',
  components: {
    ContactForm
  },
  props: {
    contact: {
      type: Object
    }
  },
  data() {
    return {
      form: { name: '', gender: '' }
    };
  },
  watch: {
    contact: {
      deep: true,
      immediate: true,
      handler(c) {
        if (! c) return;

        const { name, gender } = c || { name: '', gender: '' };
        this.form = Object.assign(this.form, { name, gender });

        this.resetForm(this.form);
      }
    }
  },
  methods: {
    submit(form) {
      if (! confirm('Are you sure?')) return;

      axios.put(`/api/contacts/${this.contact.id}`, form)
        .then(({ data: { data } }) => {
          this.$emit('contact-updated', data);

          this.form.name = data.name;
          this.form.gender = data.gender;

          this.resetForm(this.form);
        })
        .catch(({ response }) => {
          alert('Could not create');
        });
    },
    resetForm() {
      if(! this.$refs.form) return;

      this.$refs.form.resetForm(this.form);
    }
  },
}
</script>

We can now update our contacts.

You can explore all the changes made here.

In the next article, we'll add notes to our contacts.