Search code examples
cssknockout.jsdata-bindingknockout-3.2

On click a DIV apply dynamic data binding to edit the CSS using an HTML input - knockout.js


I'm trying to edit individual elements of an array. I have a JSON array like this:

[   {"idComponent":1,"left":"100px","top":"100px","idHTML":"name_div","width":"200px","height":"300px","value":"My name is"}
               ,{"idComponent":2,"left":"200px","top":"200px","idHTML":"date_div","width":"200px","height":"300px","value":"2016-Enero-12"}
               ,{"idComponent":3,"left":"300px","top":"300px","idHTML":"weigth_div","width":"200px","height":"300px","value":"1.5 KG"}
               ,{"idComponent":4,"left":"400px","top":"400px","idHTML":"talla_div","width":"200px","height":"300px","value":"23"}
               ,{"idComponent":5,"left":"500px","top":"500px","idHTML":"description_div","width":"300px","height":"300px","value":"The text"}
            ]

These are converted to observables in knockout. Each one is binded to a DIV element to display in screen. "left", "top", "height" and "width" are CSS attributes that are applied to each one.

When you click ones of the DIV contains with the mouse, I'm trying to bind the element to HTML inputs to edit the CSS values. You can see the idea in the next picture:

Mockup Image - Click here to see the image

The code is the next:

  function convertPixelsToInches(pixels){
    return pixels/96;
  }

  // Elemento regresados desde el servidor.
  var dataFromServer =
  {
     "idTemplate":"1"
    ,"components" :
      [{"idComponent":1,"left":"100px","top":"100px","idHTML":"name_div","width":"200px","height":"300px","value":"Sergio Pinto Fernandez"}
      ,{"idComponent":2,"left":"200px","top":"200px","idHTML":"date_div","width":"200px","height":"300px","value":"2016-Enero-12"}
      ,{"idComponent":3,"left":"300px","top":"300px","idHTML":"weigth_div","width":"200px","height":"300px","value":"1.5 KG"}
      ,{"idComponent":4,"left":"400px","top":"400px","idHTML":"talla_div","width":"200px","height":"300px","value":"23"}
      ,{"idComponent":5,"left":"500px","top":"500px","idHTML":"description_div","width":"300px","height":"300px","value":"Tomar dos cucharadas cada 3 hras."}
      ]
    ,"paperSize":"papersize_USLetter_portrait"
    ,"templateImage":{
      "imageUrl":"images/SolicitudLaboratorioExpress.jpg"
      ,"width":"8.5in"
      ,"height":"11in"
      ,"left":"0px"
      ,"top":"0px"
    }
  };

  function componentModel(dataComponent) {
    if (!dataComponent) {
      dataComponent = {};
    }

    var self = this;
    self.idComponent  = ko.observable(dataComponent.idComponent);
    self.left         = ko.observable(dataComponent.left);
    self.top          = ko.observable(dataComponent.top);
    self.idHTML       = ko.observable(dataComponent.idHTML);
    self.width        = ko.observable(dataComponent.width);
    self.height       = ko.observable(dataComponent.height);
    self.value        = ko.observable(dataComponent.value);
  }

  /**
   * data Json from server.
   *
   */
  function templateModel(data) {
    if (!data) {
      data = {};
    }
    var self = this;

    self.components = ExtractComponents(self, data.components, componentModel);

    self.currentSelectedComponent = ko.observable();
    self.currentSelectedComponentIndex = ko.observable(-1);

    //self.currentSelectedComponentLeft = ko.observable();
    self.currentSelectedComponentLeft = ko.computed(function(){
      var value = self.currentSelectedComponentIndex();
      console.log(typeof value);
      //value=value*1;
      console.log(value);
      if (value ) {
        return "TT";
      }

      // Get "200px y must save as 200"
      //return self.currentSelectedComponent().left;//.substring(0,data.length-2);
      return "FF";
    });
    self.currentSelectedComponentTop = ko.observable();
    self.editComponent = function(component,index){
      self.currentSelectedComponentIndex(index);
      self.currentSelectedComponent(component);

      // Binding the component to the editor.
      // ??
    };
    function bindComponentToEditor() {
      ko.applyBindings()
    }


    self.idTemplate = ko.observable(data.idTemplate);
    self.paperSize = ko.observable(data.paperSize);

    /* */
    self.paperSizeWidth = ko.observable(convertPixelsToInches(1067));
    self.paperSizeHeigth = ko.observable(convertPixelsToInches(1067));
    //
    self.templateImage_imageUrl = ko.observable(data.templateImage.imageUrl);
    self.templateImage_width = ko.observable(data.templateImage.width);
    self.templateImage_height = ko.observable(data.templateImage.height);
    self.templateImage_left = ko.observable(data.templateImage.left);
    self.templateImage_top = ko.observable(data.templateImage.top);
  }

  /**
   * parent: referencia al objeto o funcion que mando a llamar esta fucnion.
   *
   * dataArr: Array de elementos que se desea la funcion ExtractComponents haga mapeo.
   *    Ejemplo de dataArr:
   *
   *        [   {"idComponent":1,"left":"100px","top":"100px","idHTML":"name_div","width":"200px","height":"300px","value":"Sergio Pinto Fernandez"}
           ,{"idComponent":2,"left":"200px","top":"200px","idHTML":"date_div","width":"200px","height":"300px","value":"2016-Enero-12"}
           ,{"idComponent":3,"left":"300px","top":"300px","idHTML":"weigth_div","width":"200px","height":"300px","value":"1.5 KG"}
           ,{"idComponent":4,"left":"400px","top":"400px","idHTML":"talla_div","width":"200px","height":"300px","value":"23"}
           ,{"idComponent":5,"left":"500px","top":"500px","idHTML":"description_div","width":"300px","height":"300px","value":"Tomar dos cucharadas cada 3 hras."}
        ]
   *
   * modelConstructor:  funcion con que se creara un nuevo componente, es decir el modelo.
   *
   */
  function ExtractComponents(parent, dataArr, modelConstructor) {
    var components = [];
    if (dataArr == null) {
      return components;
    }

    for (var i = 0; i < dataArr.length; i++) {
      var dataArrElement = dataArr[i];
      var component = new modelConstructor(dataArrElement,parent);
      components.push(component);
    }
    return components;
  }

  ko.applyBindings(new templateModel(dataFromServer));

I have two problems:

  1. The Input for left and top values only accept integer values, the i need substring "200px" to 200 but alway i receive a error: ".left is undefined"

    self.currentSelectedComponentLeft = ko.computed(function(){ var value = self.currentSelectedComponentIndex(); console.log(typeof value); //value=value*1; console.log(value); if (value ) { return "TT"; }

      // Get "200px and save as 200"
      //return self.currentSelectedComponent().left;//.substring(0,data.length-2);
      return "FF";
    });
    
  2. The principal problem... how can I bind the DIV element when I click the element to the inputs at the right? I think I need dynamic binding, or dynamic subscription, but can't find answer to this problem of dynamic double data binding.

This is the HTML:

<div id="mainContainer">
  <div id="contentPrint_div" class="page" data-bind="css: paperSize">

    <img id="template_img" data-bind="attr: {src: templateImage_imageUrl},
      style: {width: templateImage_width, height: templateImage_height, left: templateImage_left, top: templateImage_top}">

    <div id="fieldsArea_div" class="" data-bind="foreach: components">
      <div class="field" data-bind="html: value, style: {width: width, left:left, top:top},
         attr: {id: idHTML}, click: $parent.editComponent($data,$index)"></div>
    </div>
  </div>

  <div id="toolsbar">
    <div id="toolbarPanel">
      ID template:<span data-bind='text: idTemplate'></span>

      <div id="panelMenuInfoElements_div">
        Elemento actual: <span data-bind='text: idTemplate'></span>
        Posicion
        X:<input type="text" class="form-control" id="" placeholder="x" min="0" step="0.01"
            data-bind="attr: {max: paperSizeWidth}, value: currentSelectedComponentTop">
        Y:<input type="text" class="form-control" id="" placeholder="y" min="0" step="0.01"
            data-bind="attr: {max: paperSizeHeigth}, value: currentSelectedComponentLeft">
      </div>

    </div>
    <div id="toolbarCode">
      <pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
    </div>

  </div>
</div>

Solution

  • You posted quite some code; I'm not 100% sure if I got all your questions correctly. But I tried to create an example that I think shows how to solve your problems.

    • I've used a computed to create a style object that rewrites x and y observables that are numbers, to the style binding compatible: { top: 0px, left: 0px }
    • I've used a with binding to link your <input> elements to the last clicked block.

    var Block = function(name, x, y) {
      this.label = name;
      this.x = ko.observable(x);
      this.y = ko.observable(y);
    
      this.style = ko.computed(function() {
        return {
          top: this.y() + "px",
          left: this.x() + "px"
        }
      }, this);
    }
    
    
    var VM = function() {
    
      this.blocks = [
        new Block("Block 1", 100, 100),
        new Block("Block 2", 200, 100),
        new Block("Block 3", 300, 100),
      ];
    
      this.selectedBlock = ko.observable(this.blocks[0]);
    };
    
    ko.applyBindings(new VM());
    .Block {
      position: absolute;
      background: red;
      padding: 1rem;
      list-style: none;
    }
    .Block.is-selected { background: yellow; }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    <div class="Inputs" data-bind="with: selectedBlock">
      <div>
        Editing <span data-bind="text: label"></span> 
      </div>
      <div>Left:
        <input type="number" data-bind="value: x">
      </div>
      <div>Top:
        <input type="number" data-bind="value: y">
      </div>
    </div>
    
    <ul data-bind="foreach: blocks">
      <li class="Block" data-bind="
        click: $parent.selectedBlock,   
        style: style,
        text: label,
        css: { 'is-selected': $data == $parent.selectedBlock()}"></li>
    </ul>