javascriptjqueryhtmlknockout.js

Initializing empty form breaks page in Knockout.js


Using Knockout.js I want to have forms that allow infinite choices, but I need the form to display so the user knows it exist. I'm OK with starting with 3 forms, so I'd like to initialize empty objects when the page renders. For some reason, when I initialize one object it breaks my code:

        function Task(data) {
            this.title=ko.observable(data.title);
            this.isDone=ko.observable(data.isDone);
        }

        function TaskListViewModel() {
            // Data
            var self=this;
            self.tasks=ko.observableArray([]);
            // self.tasks.push({'title': ''})
            self.newTaskText=ko.observable();
            self.incompleteTasks=ko.computed(function() {
                return ko.utils.arrayFilter(self.tasks(), function(task) {
                    return !task.isDone()
                });
            });

            // Operations
            self.addTask=function() {
                self.tasks.push(new Task({
                    title: this.newTaskText()
                }));
                self.newTaskText("");
            };

            self.removeTask=function(task) {
                self.tasks.destroy(task)
            };

            self.incompleteTasks=ko.computed(function() {
                return ko.utils.arrayFilter(self.tasks(),
                    function(task) {
                        return !task.isDone() && !task._destroy
                    });
            });

            self.save=function() {
                $.ajax(".", {
                    data: ko.toJSON({
                        tasks: self.tasks
                    }),
                    type: "post",
                    contentType: "application/json",
                    success: function(result) {
                        alert(result)
                    }
                });
            };

            // load initial state from server, convert to tasks, then add em to self.tasks
            $.getJSON(".", function(allData) {
                var mappedTasks=$.map(allData, function(item) {
                    return new Task(item)
                });
                self.tasks(mappedTasks);
            });
            
            self.tasks.push({'title': ''})
        }
        ko.applyBindings(new TaskListViewModel());
    body { font-family: Helvetica, Arial }
    input:not([type]), input[type=text], input[type=password], select { background-color: #FFFFCC; border: 1px solid gray; padding: 2px; }

    .codeRunner ul {list-style-type: none; margin: 1em 0; background-color: #cde; padding: 1em; border-radius: 0.5em;}
    .codeRunner ul li a { color: Gray; font-size: 90%; text-decoration: none }
    .codeRunner ul li a:hover { text-decoration: underline }
    .codeRunner input:not([type]), input[type=text] { width: 30em; }
    .codeRunner input[disabled] { text-decoration: line-through; border-color: Silver; background-color: Silver; }
    .codeRunner textarea { width: 30em; height: 6em; }
    .codeRunner form { margin-top: 1em; margin-bottom: 1em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<body class="codeRunner">
    <h3> Stuff </h3>
    
        <div data-bind="foreach: tasks, visible: tasks().length > 0">
            <p data-bind="value: title"></p>
        </div>
            
        <ul data-bind="foreach: tasks, visible: tasks().length > 0">
            <li>
                <input data-bind="value: title, disable: isDone" />
                <a href="#" data-bind="click: $parent.removeTask">Delete</a> 
            </li>
        </ul>
          You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s) 
          <span data-bind="visible: incompleteTasks().length == 0"> -it 's beer time!</span>
              
        <form data-bind="submit: addTask"><button type="submit">Add</button></form>

    <script>

    </script>

</body>

What is the pattern in knockout to initialize safely with this block of JS?


Solution

  • The reason you're getting an error is because the isDone property in your initial task was not being set. You also already have a Task viewModel, so why not use it to initialize your array? I've just used an IIFE (immediately-Invoked Function Expression) to initialize new tasks by newing up Task in a for loop. You can do this manually or in whichever way you prefer.

    Also be aware of your use of the this keyword. See self.addTask in your code.

    Im not sure if this is exactly what you're looking for but I assume you'd need a text input to enter newTaskText or am I missing something? Anyway, this seems to work. Hope is answers your question.

    function Task(data) {
      this.title = ko.observable(data.title);
      this.isDone = ko.observable(data.isDone || false);
    }
    
    function TaskListViewModel() {
      // Data
      var self = this;
      self.tasks = ko.observableArray([]);
      // self.tasks.push({'title': ''})
      self.newTaskText = ko.observable();
      self.incompleteTasks = ko.computed(function() {
        return ko.utils.arrayFilter(self.tasks(), function(task) {
          return !task.isDone()
        });
      });
    
      // Operations
      self.addTask = function() {
        self.tasks.push(new Task({
          title: self.newTaskText(),
          isDone: false
        }));
        self.newTaskText("");
      };
    
      self.removeTask = function(task) {
        self.tasks.destroy(task)
      };
    
      self.incompleteTasks = ko.computed(function() {
        return ko.utils.arrayFilter(self.tasks(),
          function(task) {
            return !task.isDone() && !task._destroy
          });
      });
    
      (function(numTasks) {
        for (var x = 0; x < numTasks; x++) {
          self.tasks.push(new Task({
            title: ""
          }));
        }
      })(3)
    
    }
    ko.applyBindings(new TaskListViewModel());
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    
    <body class="codeRunner">
      <h3> Stuff </h3>
    
      <input data-bind="textInput: newTaskText" type="text" />
      <input data-bind="click: addTask" type="button" value="Add Task" />
    
      <div data-bind="foreach: tasks, visible: tasks().length > 0">
        <p data-bind="value: title"></p>
      </div>
    
      <ul data-bind="foreach: tasks, visible: tasks().length > 0">
        <li>
          <input data-bind="value: title, disable: isDone" />
          <a href="#" data-bind="click: $parent.removeTask">Delete</a> 
        </li>
      </ul>
      You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
      <span data-bind="visible: incompleteTasks().length == 0"> -it 's beer time!</span>
    
    
    
      <script>
      </script>
    
    </body>