Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple knockout components in the same view

I've searched high and low for an answer to this but can't for the life of me figure out what I'm doing differently from the official example, aside from the fact that I think my use case is a bit more complicated:

http://knockoutjs.com/documentation/component-binding.html

Basically, I'm trying to create reusable UI elements. The behavior will essentially be passed to them via the "params" object. I want multiple elements to be able to exist on a single page, however, which is where I'm running into difficulty.

I'm using browserify to bundle my code and have the following (some entries truncated for brevity):

index.html

<div data-bind='component: { name: "toggle" , params: {
   enabledText: "Parental controls are enabled",
   disabledText: "Parental controls are disabled"
 }}'></div>
 <div data-bind='component: { name: "toggle" , params: {
   enabledText: "Same component, different behavior: enabled",
   disabledText: "Same component, different behavior: disabled"
 }}'></div>

main.js

var ko = window.ko = require('knockout'),
    toggle = require('./components/toggle/toggle');

ko.components.register('toggle', toggle);

function Container() {

}

var con = new Container();

ko.applyBindings(con);

components/toggle/toggle.js

var ko = require('knockout'),
    template = require('./toggle.html');

function vm(params) {
  var self = this;
  self.enabled = ko.observable(false);
  self.label = ko.computed(function() {
    return self.enabled() ? params.enabledText : params.disabledText;
  });
}

module.exports = { viewModel: vm, template: template };

And finally, in components/toggle/toggle.html:

<input type='checkbox' data-bind='checked: enabled' id='switch-checkbox' class='switch-input' />
<label for='switch-checkbox' class='switch-input-label'>
  <span data-bind='text: label'></span>
</label>

The issue I'm having is that the components both appear on the page appropriately, but clicking the second one activates the first one (and does nothing for the seconD). I'm new to Knockout and am clearly missing something, but I can't figure out how to fix my issue. Any help would be immensely appreciated!

The strange thing is that the labels are appropriately unique, indicating that the components (view models) being instantiated for each HTML entity are, in fact, unique...however, it seems knockout's "checked" binding is only binding to the first.

EDIT: I know it's customary to include an example, so here's one on codepen. I apologize for using browserified code but hopefully it's still readable:

http://codepen.io/sunny-mittal/pen/OVBNwp

like image 900
sunny-mittal Avatar asked Jul 24 '15 06:07

sunny-mittal


People also ask

Can we have multiple knockout models on a single page?

Knockout now supports multiple model binding. The ko. applyBindings() method takes an optional parameter - the element and its descendants to which the binding will be activated. This restricts the activation to the element with ID someElementId and its descendants.

What is applyBindings in knockout?

applyBindings do, The first parameter says what view model object you want to use with the declarative bindings it activates. Optionally, you can pass a second parameter to define which part of the document you want to search for data-bind attributes. For example, ko.

Which of the following architecture is implemented by knockout?

Knockout. js is a JavaScript implementation of the MVVM pattern with templates. There are several advantages of Knockout. js and its MVVM architecture design.

What is knockout ViewModel JS?

A ViewModel can be any type of JavaScript variable. In Example 1-3, let's start with a simple JavaScript structure that contains a single property called name .


1 Answers

When you load multiple instances of this component the id attribute on the input tag and the for attribute on the label tag, that make up your component, are no longer going to be unique to the page.

You essentially have two input tags with the same id and two label tags targeting one id. Your label for="switch-checkbox" on the second component is picking up input id="switch-component" from the first component and not the second.

Though knockout components are great, unfortunately there is no dom isolation between instances of components.

To resolve this issue you need to ensure the values for id and for in each instance of your component are unique to the entire page.

I have included a snippet below of this working.

var uid = function(){
  var seed = 1;
  return { 
    new: function(p){
      return p + (seed++);
    }
  }
}();


    var template =
      "<input type='checkbox' data-bind='checked: enabled, attr: {id:id}' class='switch-input' />\n<label data-bind='attr: {for: id}' class='switch-input-label'>\n  <span data-bind='text: label'></span>\n</label>\n";

    var viewModel = function vm(params) {
      var self = this;
      self.id = uid.new('switch-checkbox-');
      self.enabled = ko.observable(false);
      self.label = ko.computed(function() {
        return self.enabled() ? params.enabledText : params.disabledText;
      });
    }

    var component = {
      viewModel: viewModel,
      template: template
    };

    ko.components.register('toggle', component);

    var vm = {};

    ko.applyBindings(vm);
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html {
  position: relative;
  height: 100%;
  min-height: 100%;
}
.switch-input {
  display: none;
}
.switch-input-label {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  -o-user-select: none;
  user-select: none;
  position: relative;
  display: inline-block;
  cursor: pointer;
  font-weight: 500;
  text-align: left;
  margin: 16px;
  padding: 16px 0 16px 44px;
}
.switch-input-label:before,
.switch-input-label:after {
  content: '';
  position: absolute;
  margin: 0;
  outline: 0;
  top: 50%;
  transform: translate(0, -50%);
  transition: all 0.3s ease;
}
.switch-input-label:before {
  left: 1px;
  width: 34px;
  height: 14px;
  background-color: #9e9e9e;
  border-radius: 8px;
}
.switch-input-label:after {
  left: 0;
  width: 20px;
  height: 20px;
  background-color: #fafafa;
  border-radius: 50%;
  box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.098), 0 1px 5px 0 rgba(0, 0, 0, 0.084);
}
.switch-input:checked + .switch-input-label:before {
  background-color: #a5d6a7;
}
.switch-input:checked + .switch-input-label:after {
  background-color: #4caf50;
  transform: translate(80%, -50%);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>


<div data-bind='component: { name: "toggle" , params: {
   enabledText: "Parental controls are enabled",
   disabledText: "Parental controls are disabled"
 }}'></div>

<div data-bind='component: { name: "toggle" , params: {
   enabledText: "Same component, different behavior: enabled",
   disabledText: "Same component, different behavior: disabled"
 }}'></div>

<div data-bind='component: { name: "toggle" , params: {
   enabledText: "Same component, another instance: enabled",
   disabledText: "Same component, another instance: disabled"
 }}'></div>
like image 169
Anish Patel Avatar answered Sep 19 '22 08:09

Anish Patel