Using the Command Pattern to Re-Apply Changes to an Observable Model in Umbraco Bellissima

Using the Command Pattern to Re-Apply Changes to an Observable Model in Umbraco Bellissima

In Umbraco v14 one of the hot new approaches is observable models.

You can think of observables as a long lived promise that can resolve multiple times. So when you request a model from a repository, it will usually return an observable of that model. You would then subscribe to this observable to retrieve it’s value and store it in a local variable to do what you need to do with it.

If the model ever changes in the repository, your observable subscription will be notified again and you can update your local reference.

Here is some pseudo code to show how this might look:

export class MyComponent extends UmbLitElement { // Create a local reference to my repository private _modelRepo: MyModelRepo = new MyModelRepo(this); // Create a local variable for our model @state() private _model?: MyModel; constructor() { super(); // Fetch the model as an observable const model$ = this._modelRepo.getModel(); // Observe the observable model and store it's value locally on change this.observe(model$, (model) => this._model = model); } render() { // Render your model here } } 
Enter fullscreen mode
Exit fullscreen mode

This is great for components that just render out the model in some way as this gives your component reactivity ensuring it always displays the most up to date data.

When this can be problematic however is when you have en editor interface to your model. If your model has changed due to user input, but you then receive an update from your observable, if you simply update your local model reference, all your user changes will be lost.

The way I have solved this in Umbraco Commerce is to make use of the command pattern.

The Command Pattern

With the command pattern, where you might previously have modified your model directly based on user input, you instead encapsulate that modification into a “Command” class.

So where before we might have just done the following:

this._model.billingAddress = { line1: "Some Street", city: "Some City", zipCode: "Some Zip Code"
};
Enter fullscreen mode
Exit fullscreen mode

We instead move this command into it’s own class like so:

export class UdateModelBillingAddressCommand { address:Address; constructor(address:Address) { this.address = address; } execute(receiver:MyModel) { receiver.billingAddress = { ...this.address }; }
}
Enter fullscreen mode
Exit fullscreen mode

And then we make changes to our model via the command instead:

const newAddress = { line1: "Some Street", city: "Some City", zipCode: "Some Zip Code"
};
const cmd = new UdateModelBillingAddressCommand(newAddress);
cmd.execute(this._model);
Enter fullscreen mode
Exit fullscreen mode

You might be thinking this is a lot more code for the same outcome, and yes, it is more verbose, but by encapsulating our updates in this way, we can store them and reuse them.

Command Base Class

Before we move onto the next step, we’ll want to create a Command base class that defines a predictable API for our commands.

export abstract class Command<TReceiver> { execute(receiver:TReceiver);
}
Enter fullscreen mode
Exit fullscreen mode

Now we can update our previous command like so:

export class UdateModelBillingAddressCommand extends Command<MyModel> { address:Address; constructor(address:Address) { this.address = address; } execute(receiver:MyModel) { receiver.billingAddress = { ...this.address }; }
}
Enter fullscreen mode
Exit fullscreen mode

And ensure any future commands also do the same:

export class UdateModelShippingAddressCommand extends Command<MyModel> { address:Address; constructor(address:Address) { this.address = address; } execute(receiver:MyModel) { receiver.shippingAddress = { ...this.address }; }
}
Enter fullscreen mode
Exit fullscreen mode

Storing Commands

With our commands now implementing this base class, it allows us to easily maintain a collection of commands, and know that we can always call an execute method passing a reference to our model:

private _commands = new Array<Command<MyModel>>();
Enter fullscreen mode
Exit fullscreen mode

Our collection never cares what the commands are / do. All it is bothered about is that they all have an execute command it knows how to call.

With our collection defined, now when we update our model, we can also push the command to our commands collection:

// Update our model as before
const newAddress = { line1: "Some Street", city: "Some City", zipCode: "Some Zip Code"
};
const cmd = new UdateModelBillingAddressCommand(newAddress);
cmd.execute(this._model); // But now also push our command to the commands collection
this._commands.push(cmd);
Enter fullscreen mode
Exit fullscreen mode

So now whenever an update is performed, we update our current local model, but also store a copy of the command that was executed in our list of commands.

Re-Applying Commands

With our collection of commands, we now effectively have a history of all changes that have been applied to our model, and so this is where the power of the command pattern comes into play.

We can now update our observer subscription to not just store the new model value, but also re-apply all of our commands.

this.observe(model$, (model) => { this._commands.forEach(command => command.execute(model)); this._model = model;
});
Enter fullscreen mode
Exit fullscreen mode

And with that we have a newly updated model, with our user modifications still maintained.

The last thing you’ll want to do is update your persistence method to clear the commands list once your changes are saved.

save() { this._modelRepo.save(this._model); this._commands = [];
}
Enter fullscreen mode
Exit fullscreen mode

Gotchas

The only real gotcha I can think of is any occasion where a change coming from the observable is such that a command is no longer able to execute. In those situations I think it would be reasonable to wrap the logic that re-applys commands in a try/catch and if this fails, raise a notification saying something along the lines of “The model you are editing has changed and we are unable to re-apply your changes. Please refresh and try again”.

I hope this solution might come in handy for others too.


Discover more from Coursity

Subscribe to get the latest posts to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *