Knockout: the best way to bind visibility to both element and parent property?
I am creating an edit screen where I want people to remove items from the list. The list is displayed normally until the controller object enters edit mode. The user can then delete the items. The items should be marked for deletion and displayed as such, then when the user saves the edit, they are deleted and the server is notified.
I actually have it all working, but the only way to do it is to use literal conditions in the bindings, which looks ugly and I don't really like it. Is there a better way to do this?
Working fiddle: http://jsfiddle.net/L1e7zwyv/
Markup:
<div id="test">
<a data-bind="visible: IsViewMode, click: edit">Edit</a>
<a data-bind="visible: IsEditMode, click: cancel">Cancel</a>
<hr/>
<ul data-bind="foreach: Items">
<li data-bind="css: CssClass">
<span data-bind="visible: $parent.IsViewMode() || $data._Deleting(), text: Value"></span>
<!-- ko if: $parent.IsEditMode() && !$data._Deleting() -->
<input type="text" data-bind="value: Value"/>
<a data-bind="click: $parent.deleteItem">Del</a>
<!-- /ko -->
</li>
</ul>
</div>
code:
function ItemModel(val)
{
var _this = this;
this.Value = ko.observable(val);
this._Deleting = ko.observable();
this.CssClass = ko.computed(
function()
{
return _this._Deleting() ? 'deleting' : '';
}
);
}
function ManagerModel()
{
var _this = this;
this.Items = ko.observableArray([
new ItemModel('Hell'),
new ItemModel('Broke'),
new ItemModel('Luce')
]);
this.IsEditMode = ko.observable();
this.IsViewMode = ko.computed(function() { return !_this.IsEditMode(); });
this.edit = function(model, e)
{
this.IsEditMode(true);
};
this.cancel = function(model, e)
{
for(var i = 0; i < _this.Items().length; i++)
_this.Items()[i]._Deleting(false);
this.IsEditMode(false);
};
this.deleteItem = function(model, e)
{
model._Deleting(true);
};
}
ko.applyBindings(new ManagerModel(), document.getElementById('test'));
source to share
You can:
- wrap another range to separate the bindings, but this will be less efficient.
- Use as an anchor
visible:
, andif:
for one and the same element to achieve the same functionality, - write a function in itemModel
isVisible()
to take the parent as an argument making your bindingvisible: $data.isVisible($parent)
. - Afterthought: if this appears in multiple places you can write a helper function to combine the visibility bindings
// reprisent variables from models var v1 = false; var v2 = false; var v3 = false; // Helper functions defined in main script body - globally accessible function VisibilityFromAny() { var result = false; for(var i = 0; i < arguments.length; i++ ) result |= arguments[i]; return Boolean(result); } function VisibilityFromAll() { var result = true; for(var i = 0; i < arguments.length; i++ ) result &= arguments[i]; return Boolean(result); } // represent bindings alert(VisibilityFromAny(v1, v2, v3)); alert(VisibilityFromAll(v1, v2, v3));
The third option is the most popular method with MVVM aficionados like you to combine variables into one binding from what I've seen, it makes sense and keeps all the logic from the view markup in view models.
Personally, I like the syntax you have at the moment (although I consider myself among the group of MVVM fans), it clearly shows in the markup of the view that the visibility of this element is bound to two elements, and does not hide this data in the function.
source to share
I am trying to represent view models as a model for my view, not just a place where logic resides. Whenever possible, I also try to move the complex logic back into the view model and use descriptive names for my variables to make the code more readable.
I would suggest adding this to your viewmodel -
var isViewable = ko.computed(function () { return IsViewMode() || _Deleting(); });
var isEditable = ko.computed(function() { return IsEditMode() && !_Deleting(); });
And in your opinion -
<li data-bind="css: CssClass">
<span data-bind="visible: isViewable, text: Value"></span>
<!-- ko if: isEditable -->
<input type="text" data-bind="value: Value"/>
<a data-bind="click: $parent.deleteItem">Del</a>
<!-- /ko -->
</li>
This clears up the bindings and allows you to customize your logic more easily without doing a lot of sanity checks on your view and treating the model as. Also, I personally name variables that return a boolean value such as this, however, to help describe in more detail.
The advantage is that as your view model and view grows, you can keep the DOM clean of clutter and also your view model becomes testable .
Here is a consolidated version of your fiddle with a full addition -
source to share