6

Problem: I'm trying to build a dashboard of widgets. Each widget will have a delete button on its header. When clicked on this button, corresponding widget have to disappear.

How I designed: I have two knockout components.

  1. my-widget-list: VO will have an observableArray of widget objects.
  2. my-widget: VO will have details to display within the widget.

Note: For simplicity, I'm replacing the widget object with just numbers.

ko.components.register('my-widget-list', {       
    viewModel : function(params) {
        var self = this;
        self.values = ko.observableArray([10,20,30,40,50]);

        self.deleteWidget = function(obj)
        {
            self.values.remove(obj);
        }
    },
    template: {element: 'my-widget-list-template'}
});

ko.components.register('my-widget', {
    viewModel : function(params) {        
        var self = this;        
        self.value = params.value;                        
    },
    template: {element: 'my-widget-template'}
});

ko.applyBindings({}); 
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<my-widget-list></my-widget-list>

<script id="my-widget-list-template" type="text/html">
    <div data-bind="foreach:values">
        <my-widget params="value: $data"></my-widget><br>
    </div>
</script>

<script id="my-widget-template" type="text/html">
    <span data-bind="text: value"></span>
    <button data-bind="click: $parent.deleteWidget">Delete</button>
</script>

Now, I want to invoke my-widget-list's deleteWidget function when the button is clicked.

I have thought about

  • Passing the parent view model reference into the child
  • Passing the parent function in the params attribute of the child component as a callback

But I wish to know from experts what's the best way to achieve this.

JsFiddle Link

Thanks in advance

Tomalak
  • 332,285
  • 67
  • 532
  • 628
Thaha
  • 303
  • 3
  • 7

1 Answers1

9

You can pass in the parent as a param to the child:

ko.components.register('my-widget-list', {       
    viewModel : function(params) {
        var self = this;
        self.values = ko.observableArray([10,20,30,40,50]);

        self.deleteWidget = function(obj) {
            self.values.remove(obj);
        }
    },
    template: {element: 'my-widget-list-template'}
});

ko.components.register('my-widget', {
    viewModel : function(params) {        
        var self = this;        

        self.value = params.value;
        self.remove = function () {
            params.parent.deleteWidget(self.value);
        };
    },
    template: {element: 'my-widget-template'}
});

ko.applyBindings({});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<my-widget-list></my-widget-list>

<script id="my-widget-list-template" type="text/html">
    <div data-bind="foreach:values">
        <my-widget params="value: $data, parent: $parent"></my-widget><br>
    </div>
</script>

<script id="my-widget-template" type="text/html">
    <span data-bind="text: value"></span>
    <button data-bind="click: remove">Delete</button>
</script>

But I'm not sure if that is a good idea, as it needlessly couples the child to the parent.

I'd recommend implementing the "remove" button in the parent, i.e. in <my-widget-list>, this way the widget can exist without a widget-list (or in a differently structured one) while the widget-list is in control of its children.

Compare window managers: They work the same way. The window manager draws the frame and the minimize/maximize/close buttons, while the window contents is drawn by the respective child process. That logic makes sense in your scenario as well.


Alternative implementation with removeWidget control in the parent:

ko.components.register('my-widget-list', {
    viewModel : function(params) {
        var self = this;

        self.values = ko.observableArray([10,20,30,40,50]);

        self.deleteWidget = function(obj) {
            self.values.remove(obj);
        }
    },
    template: {element: 'my-widget-list-template'}
});

ko.components.register('my-widget', {
    viewModel : function(params) {
        var self = this;

        self.value = params.value;
    },
    template: {element: 'my-widget-template'}
});

ko.applyBindings({});
.widget-container {
  position: relative;
  display: inline-block;
  padding: 10px 5px 5px 5px;
  margin: 0 5px 5px 0;
  border: 1px solid silver;
  border-radius: 2px;
  min-width: 40px;
}
.widget-buttons {
  position: absolute;
  top: 2px;
  right: 2px;
}
.widget-buttons > button {
  font-size: 2px;
  padding: 0;
  height: 15px;
  width: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<my-widget-list></my-widget-list>

<script id="my-widget-list-template" type="text/html">
    <div class="widget-list" data-bind="foreach:values">
        <div class="widget-container">
            <div class="widget-buttons">
                <button data-bind="click: $parent.deleteWidget">X</button>
            </div>
            <my-widget params="value: $data"></my-widget>
        </div>
    </div>
</script>

<script id="my-widget-template" type="text/html">
    <div class="widget">
        <span data-bind="text: value"></span>
    </div>
</script>
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Thank you @tomalak for the quick response. Regarding "remove" button to be in the parent, I'm not sure how I can implement that. Each widget need to have a 'X' button to remove it from the dashboard. So, I thought it would be appropriate to have it within the widget view. – Thaha Jul 09 '15 at 09:37
  • You can have the `X` button inside the parent's `
    ` and position it via CSS. Make that DIV `position:relative;` and the button `position: absolute; top: 0px; right: 0px:`
    – Tomalak Jul 09 '15 at 09:45
  • wow!. I never thought about that CSS trick. Thanks @Tomalak. I will give it a try. – Thaha Jul 09 '15 at 09:55
  • @Thaha See my edit above, something along those lines. It surely can be made more visually appealing with a little effort on the CSS side. – Tomalak Jul 09 '15 at 10:01
  • Oh, that was super quick. Thank you for demonstrating with a slick implementation. I'm humbled. – Thaha Jul 09 '15 at 10:19
  • @Thaha No problem, you are welcome. :) Don't forget to mark the answer as accepted to close the thread. – Tomalak Jul 09 '15 at 16:00
  • Unfortunately, this doesn't work, when `self.remove = function () { params.parent.deleteWidget(self.value); };` change to `self.remove = function (widget) { params.parent.deleteWidget(widget); };` – Yurii N. Aug 17 '17 at 17:00
  • @YuriyN. Click "Run code snippet". Test it. It works. If anything, `deleteWidget` could be named to `removeValue`. – Tomalak Aug 17 '17 at 17:32