Passion & Opportunity ? continue : break

How to use MVVM (Model-View-ViewModel) pattern to develop UI?
Written: 2019-04-24 15:30:09 Last update: 2019-08-22 21:32:36

MVVM (Model View ViewModel) demo : Tic-Tac-Toe game.

Korean translation

MVVM (Model View ViewModel) 데모 : Tic-Tac-Toe 게임.

It is difficult to make a tie (no winner), try to make first move!

MVVM (Model-View-ViewModel) pattern is an event-driven programming paradigm to create UI (User Interface).

  • Model is data, data could be an object (eg: a number '8' or a text 'hello') or a collection of objects with various types (may be a combination of numbers, texts, images, etc.).
  • View is the UI, basic purposes of UI are
  • ViewModel is the main program logic, it controls both 'Model' and 'View'
    • Bind: to tie event to UI elements (eg: button, text label, input, etc.), logic example:
      • Register 'click' event to a button.
      • Process the 'click' event (eg: update data).
      the flow is from 'View' (UI element) to 'Model (data)
      a triggered event from UI element ('View') may also trigger update to data ('Model').
    • Observe: to monitor 'Model' (data) changes, an update to data may send notification to update 'View' (UI element).
      the flow is from 'Model' to 'View'

UI development is to focus on event, application will only start to do something when an event is occurred, so the development must be focused on how to define/create event, bind event to UI elements and react to event, hence the name is event-driven programming.

In MVVM pattern, the 'Model' and 'View' are totally separated, inside 'Model' there is no data related to UI component (eg: button element, size, coordinate, etc.), therefore 'Model' can not modify 'View' directly, and 'View' also does not know how to get the data from 'Model', so only 'ViewModel' can give data to 'View' and also only 'ViewModel' can create/read/update/delete (CRUD) data to 'Model'.

MVVM pattern to develop UI has some similarities to

The TicTacToe game is a good demonstration to show how this MVVM pattern works for web front-end UI development, this classic and simple game is created by writing a little Javascript code, so small that even a beginner to Javascript can understand and trace the code, in most MVVM web page the 'Model' data comes from a server but in this MVVM demonstration, the 'Model' data is generated by 'Human' and 'Computer' players.

Before we see the code I want to introduce a small front-end library called KnockOutJS or KO (KnockOut), it is a small Javascript library to provide an easy way to create a web page using MVVM pattern, KnockOut share some similarities to other front-end libraries such as Angular, React, Vue and others, this article will not get into detail comparison of these libraries. The number 1 reason I choose KO is that the HTML page can pass 100% W3C HTML5 validation due to KO binds HTML element tags using attribute 'data-bind' which compliant to HTML5 spec.

This demo uses only some of KO main features, Observables and Bindings to simplify the Javascript code.

Korean translation

MVVM (Model-View-ViewModel) 패턴은 UI (User Interface)를 만들기 위한 'event-driven' 방식의 프로그래밍입니다.

  • Model은 데이터를 뜻하며 데이터는 한 개의 대상 (예: 숫자 '8' 또는 텍스트 'hello') 또는 여러가지 대상 (숫자, 텍스트, 이미지 등과 같은 다양한 유형)을 의미합니다.
  • View는 'UI'를 뜻하며 UI의 목적은 아래와 같습니다.
  • ViewModel은 주 프로그램 로직이며, 'Model' 과 'View' 를 모두 제어합니다
    • Bind(바인딩)은 event와 UI 요소(버튼, 텍스트 라벨, 인풋 등)를 묶어줍니다. 그 예로는
      • '클릭'event를 버튼으로 등록
      • '클릭'event를 했을 경우 실행(예: 데이터 업데이트)
      Bind의 플로우(flow)는 'View'(UI 요소)부터 'Model'(데이터)까지를 말합니다.
    • Observe는 'Model' (데이터)의 변화를 모니터링 하기위해, 데이터 업데이트가 'View' (UI 요소)를 업데이트하도록 합니다.
      Observe의 플로우(flow)는 'Model'부터 'View'까지를 말합니다.

UI 개발은 event에 초점을 맞출 때, 앱은 event가 발생할 때에만 어떤 기능을 시작하므로, 개발은 event를 생성하고, 바인딩하며, event에 반응합니다. 따라서 이런 프로그램 방식을 'event-driven'이라고 합니다.

MVVM 패턴에서 'View'와 'Model'은 완전히 분리되어 있으며, 'Model'안에 UI 구성요소와 관련된 데이터는 없습니다(예: 버튼 요소, 크기, 좌표 등). 그래서 'Model'은 'View'를 직접 수정할 수 없고, 또한 'View'는 'Model'로부터 데이터를 가져올 수 없습니다. 그러므로 오직 'ViewModel'만이 'View'에 데이터를 보낼 수 있고, 'Model'에게 생성/읽기/업데이트/삭제를 시킬 수 있습니다.

UI 개발을 위한 MVVM의 개념은 다음과 유사합니다

TicTacToe 게임은 이 MVVM 패턴이 웹 front-end UI 개발에 어떻게 작용하는지 보여주는 좋은 데모입니다. 이러한 고전적이며 단순한 게임은 단지 이러한 적은 Javascript 코드를 쓰는 것만으로 만들어질 수 있습니다. 이 코드는 심플하기때문에 초보자도 쉽게 이해하고 추적할수있습니다. 대부분의 MVVM web-page에서 'Model' 데이터는 서버에서 나오지만 MVVM 데모의 'Model'데이터는 'Human' 및 'Computer'플레이어에 의해 생성됩니다.

코드를 보기 전에 KnockOutJS 또는 KO (KnockOut) 라는 작은 front-end 라이브러리를 소개하겠습니다. 이 라이브러리는 MVVM 패턴을 사용하여 웹 페이지를 쉽게 만들 수 있는 작은 Javascript 라이브러리입니다. KnockOut은 Angular, React, Vue와 같은 다른 front-end 라이브러리와 몇 가지 유사점을 가지고 있습니다. 이 기사는 이러한 라이브러리에 대한 세부 비교를 하지 않습니다. 제가 KO를 선택하는 첫번째 이유는 KO가 HTML 요소 태그를 HTML5의 'data-bind' 속성을 사용하여 바인딩함으로써 이 HTML 페이지가 100% W3C HTML5 유효성 검사를 통과할 수 있기 때문입니다.

이 데모에서는 KO의 일부 주요 기능인 ObservablesBindings 만 사용하여 Javascript 코드를 단순화 시켜줍니다.

How Model-View-ViewModel work in Tic-Tac-Toe?

Korean translation

Tic-Tac-Toe에서 Model-View-ViewModel은 어떻게 작동할까요?

  • 'Model': data for the game, they are Javascript variables
    Korean translation 'Model': 게임의 데이터 (Javascript 변수)
    // check if KnockOutJS existed and loaded or not
    this.isKnockOutLoaded = 'ko' in window ? true : false;
    
    // boolean: true/false
    this.playing = this.isKnockOutLoaded ? ko.observable(false) : false;
    
    // string/text
    this.gameMessage = this.isKnockOutLoaded ? ko.observable('') : '';
    
    // array content of 3x3 = 9 positions which store 'character' (should be either 'X' or 'O')
    // init value 9 position with empty value (no character)
    this.characterPositions = this.isKnockOutLoaded ? ko.observableArray(Array(9)) : Array(9);
    
    We check if 'ko' (KnockOutJS) is loaded in window, if loaded then we declared 3 KnockOut's 'observable' (variable), we use 'observable' so that if these data is changed then UI element may be updated too (see the HTML code in this demo).
    Korean translation (KnockOutJS) 'ko'가 window에 로드되었는지 확인하고, 로드되면 ko의 'observable' 3개 변수를 생성하기, 그래서 'Observable' 값의 변경되면 UI 요소 도 변경됨니다. (이 데모의 HTML 코드 참조)
  • 'View': the game is using 3x3 grid (9 buttons) also this game have other UI elements, suc as 'start' button, 'stop' button, text lable (to display message), etc., the HTML code snippet is
    Korean translation 'View': 게임 UI는 3x3 그리드(9버튼)를 사용하고 있으며, 다른 UI 요소를 예를들어서 'Start/Stop' 버튼 및 텍스트 라벨 도 가지고 있습니다. HTML 코드는
    <div id='tictactoe-game-board'
        style='display:grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: repeat(3, 80px);
            grid-gap: 2px 2px;
            background-color: #000;
            border: 1px solid #f00;
            padding: 3px;
            border-radius: 5px'
        data-bind='foreach: characterPositions'>
    
      <div class='tictactoe-button'
          style='display: flex;
            justify-content: center;
            align-items: center;
            background-color: #fff;
            color: #f00;
            padding: 15px;
            border: 1px solid #f00;
            border-radius: 5px'
          data-bind='text: $root.get($index()),
            click: $root.set.bind($root, $root.CHARS[0], $index())'></div>
    </div>
    
    <div id='label-game-message'
      style='padding: 1rem 0;'
      data-bind='text: $root.gameMessage'></div>
    
    <button id='btn-start-human'
      style='padding: 10px;'
      data-bind='visible: !$root.playing(),
                click: $root.gameStart.bind($root, $root.CHARS[0])'>
        You (X) start
    </button>
    
    <button id='btn-start-computer'
      style='padding: 10px;'
      data-bind='visible: !$root.playing(),
                click: $root.gameStart.bind($root, $root.CHARS[1])">
        Computer (O) start
    </button>
    
    <button id='btn-game-stop'
      style='padding: 10px;'
      data-bind='visible: $root.playing(),
                click: $root.gameStop">
        Stop game
    </button>
    
    Notice the attribute 'data-bind' above, they are only use to bind to KnockOut.
    1. UI element with id='tictactoe-game-board' is the game board container, it uses a grid with 3 columns by 3 rows, the attribute data-bind='foreach: characterPositions' to tell KO that we want to bind this 'UI element' with the data ('Model') called 'characterPositions'.
    2. UI element with class='tictactoe-button' is a button which will be automatically repeated as many as 'characterPositions' (in this case it is an array of 9, so total button is 9). Each button's text value is binded and will be automatically changed from method get($index()) and also each button has 'click' event registered to call method set(...)
    3. UI element with id='label-game-message' is a text label to display game message, the text is binded and will be automatically changed from method gameMessage
    4. id='btn-start-human', id='btn-start-computer' and id='btn-game-stop' are 3 buttons to control the game, these buttons visibility will be controlled by the method 'playing' and each buttons registered to 'click' event to call predefine methods (eg: gameStart, gameStop).
    Korean translation 위의 'data-bind' 특성은 KnockOut에 바인딩하는 데만 사용됩니다.
    1. UI 요소 id='tictactoe-game-board'는 3열×3행의 그리드를 사용하는 게임 보드의 컨테이너이며, 이 컨테이너를 'characterPositions'라는 데이터 ('Model')와 바인딩하려는 KO에게 알려주려면 data-bind='foreach: characterPositions 속성을 사용합니다.
    2. class='tictactoe-button'은 'characterPositions'총 수만큼 자동으로 반복되는 버튼입니다. 총 배열은 9이므로 총 버튼은 9입니다. 각 버튼 'text'는 get(index) 메소드를 호출하고 메소드 set(...)을 호출하기위한 'click'이벤트에 대한 listener를 설정하여 변경됩니다.
    3. id='label-game-message'는 게임 메시지를 표시하는 컨테이너이며, 메시지는 gameMessage 메소드에 의해 변경됩니다.
    4. id='btn-start-human', id='btn-start-computer'id='btn-game-stop' 은 게임을 제어하는 3 가지 버튼이며, 이러한 버튼 가시성은 'playing' 메소드에 의해 제어되며, 각 버튼은 'click'이벤트를 정의(gameStart, gameStop 등)된 메소드를 호출하도록 등록됩니다.
  • 'ViewModel': this is the main game logic.
    Korean translation 'ViewModel': 이것은 게임 논리입니다.

Please notice the Javascript code below is full complete code, the code may seem very long but actually very little, I've added so many comments to let others understand the logic easily, so without the comments and logs then the code will be very short.

Korean

아래의 Javascript 코드는 전체 코드이며, 코드는 매우 길어보이지만 실제로는 그렇지 않습니다. 사람들이 논리를 쉽게 이해할 수 있도록 많은 로그와 주석을 추가했습니다. 로그와 주석이 없으면 코드가 길지 않습니다.

// Game 'ViewModel'
function TicTacToe() {
  'use strict';

  // constant characters list
  this.CHARS = ['X', 'O'];

  this.isKnockOutLoaded = 'ko' in window ? true : false;

  // declare 3 data variable as observables
  // any changes to these data ('Model') will immmediately update UI ('View').

  // boolean: true/false
  this.playing = this.isKnockOutLoaded ? ko.observable(false) : false;

  // string/text
  this.gameMessage = this.isKnockOutLoaded ? ko.observable('') : '';

  // array content of 3x3 = 9 positions which store 'character' (should be either 'X' or 'O')
  // init value 9 position with empty value (no character)

  this.firstTimeOnlyData = ['','User (interact)', '↘ (click, scroll, etc.)', 'Model (data)', '', 'View (UI)', 'write ↖ ↘ read', 'ViewModel (logic)', 'data ↗ ↙ event'];
  this.characterPositions = this.isKnockOutLoaded ? ko.observableArray(this.firstTimeOnlyData) : Array(9);

  // constant winning positions
  this.winningPositions = [
      // horizontal
      [0,1,2],
      [3,4,5],
      [6,7,8],
      // vertical
      [0,3,6],
      [1,4,7],
      [2,5,8],
      // diagonal
      [0,4,8],
      [2,4,6],
    ];

  // get character from position
  this.get = function(position) {
    let char = this.characterPositions()[position];
    //console.log('get(' + position + '): ' + char);
    return char;
  }

  // set character into position
  this.set = function(character, position) {

    if( ! this.playing()) {
      console.log('can not setPoint(' + character + ', ' + position + ') because not playing');
      return;
    }

    if(0 > this.CHARS.indexOf(character)) {
      throw new Error('set(character: ' + character + ', ' + position + '), character is invalid');
    }

    if(0 > position || position > 8) {
      throw new Error('set(character: ' + character + ', ' + position + '), position is invalid');
    }

    // check if this position already occupied ?
    if(this.CHARS.indexOf(this.characterPositions()[position]) >= 0) {
      console.log('can not set(' + character + ', ' + position + ') because already occupied by: ' + this.characterPositions()[position]);
      return;
    }

    // set character
    let currentPositions = this.characterPositions(); // get
    currentPositions[position] = character; // update
    this.characterPositions(currentPositions); // set

    if(character == this.CHARS[0]) {
      // this is human player move, is it win ?
      if(this.isWin(this.CHARS[0])) {
        return;
      }

      // next move is computer
      let rnd = this.chooseRandomAvailablePosition();
      if(rnd >= 0) {
        this.set(this.CHARS[1], rnd);

        if(this.isWin(this.CHARS[1])) {
          return;
        }
      }
    }

    // is game finished (board full) ?
    let boardIsFull = this.characterPositions().every(char => this.CHARS.indexOf(char) >= 0);
    if(boardIsFull) {
      this.gameMessage('Game finished, no winner (tie)');
        this.playing(false);
    }
  }

  // is this character win ?
  this.isWin = function(character) {

    let totalWinningPosition = this.winningPositions.length;

    let currentArray = this.characterPositions();

    let i;

    for(i = 0; totalWinningPosition > i; i++) {
      if(currentArray[this.winningPositions[i][0]] == character) {
        // found the first item, check the second and third

        if(currentArray[this.winningPositions[i][1]] == character
          && currentArray[this.winningPositions[i][2]] == character) {
            // found a winning position, who is the winner ?
            if(character == this.CHARS[0]) {
              // human player win
              this.gameMessage('Congratulation, you won !');
            } else {
              this.gameMessage('Oh sorry you lose, please try again ^.^');
            }

            // stop game
            this.playing(false);

            return true;
        }
      }
    }

    return false; // at this point, not win
  }

  // find a empty position randomly
  this.chooseRandomAvailablePosition = function() {

    let currentArray = this.characterPositions();

    // find empty slot in board
    let allSlotsAreFull = currentArray.every(function(value) {
      if(0 > this.CHARS.indexOf(value)) {
        // this slot is empty
        return false;
      }
      return true;
    }.bind(this));

    // is there empty spot ?
    if(allSlotsAreFull) {
      // no more empty slot
      return -1;
    }

    let rnd;

    // loop until find empty slot
    while(true) {
      // Math.random() return '0.xxxx', so we remove '0.'
      let chars = Math.random().toString().substr(2).split('');

      let randomAvailableSlotNotFound = chars.every(function(char) {
        // at this point, char is '0' to '9'
        rnd = parseInt(char);

        // we only have 9 slots (0 to 8) (index '9' is not used)
        if(rnd >= 0 && 8 >= rnd) {
          if(0 > this.CHARS.indexOf(currentArray[rnd])) {
            // this slot is neither 'X' nor 'O', we found random empty slot
            return false; // finish loop
          }
        }

        return true;
      }.bind(this));

      if(!randomAvailableSlotNotFound) {
        return rnd;
      }
    }

    // this line should not come, if come here then it means there is a BUG in logic !!
    throw new Error ('failed to choose random available position .. BUG in logic !!');
  }

  this.gameStart = function(character) {
    this.resetGame();

    this.playing(true);

    if(character == this.CHARS[1]) {
      // computer start
      this.set(this.CHARS[1], this.chooseRandomAvailablePosition());
    }

    this.gameMessage('Now is your move (X)');
  }

  this.gameStop = function() {
    this.resetGame();
  }

  // reset all games data
  this.resetGame = function(characters) {
    console.log('resetGame()');

    // create array of 9 characters
    if(characters && characters.length == 9) {
      this.characterPositions(characters);
    } else {
      // no parameter, so set all values as empty
      this.characterPositions(Array(9).fill(''));
    }

    this.playing(false);

    this.gameMessage('To play game, please choose who will start?');
  }
}

Even a beginner front-end developer can see that Javascript code above has no UI operation to get element document.getElementById(...) and also no element event binding element.addEventListener('click', ...), these all are not necessary anymore because KnockOut has 2-way data binding so it already has done all the hard work for us, we don't need to write complex UI 'boilerplate' code anymore.

How difficult or complex is to do this without any library and only use vanilla Javascript? well let's see the vanilla Javascript code below.

Korean translation

모든 front-end 개발자는 위의 Javascript 코드에 getElementById(...) 또는 addEventListener(...)가 없음을 알수 있습니다. KnockOut은 2-ways 데이터 바인딩을 사용하므로 getElementById 과 addEventListener 더 이상 필요하지 않습니다. 복잡한 UI 'boilerplate' 코드를 더 이상 작성할 필요가 없습니다.

KnockOut library 사용없이 작업을 수행하고 vanilla Javascript 만 사용하는 것이 얼마나 복잡 할까요? 아래 vanilla Javascript 코드를 봅시다.

var gameViewModel;
function loadTicTacToe() {
  if(typeof(TicTacToe) == 'undefined') {
    return;
  }
  // define the ViewModel
  gameViewModel = new TicTacToe();

  // use KnockOut to bind the ViewModel
  if('ko' in window) {
    console.log('KnockOut is already loaded in window, so use KnockOutJS');

    ko.applyBindings(gameViewModel);

    // init game's variables
    gameViewModel.resetGame(gameViewModel.firstTimeOnlyData);
  } else {
    console.log('KnockOut is not existed or not loaded, so init game without KnockOutJS');

    /************************************************************************
    ***** below is additional code if 'KnockOutJS' is not used/loaded ******
    ************************************************************************/

    TicTacToe.prototype.initTicTacToeWithoutKnockOut = function(boardContainerId, gameStartHumanBtnId, gameStartComputerBtnId, gameStopBtnId, gameMessageLableId) {
      console.log('initTicTacToeWithoutKnockOut(...)');

      let eleGameBoard = document.getElementById(boardContainerId);

      if(eleGameBoard.children.length == 1) {
        // repeat content (should be only 1 button) to 9 times
        let buttons = Array(9).fill(eleGameBoard.innerHTML).reduce((prev, curr, index) => (prev ? prev + curr : curr));

        // set
        eleGameBoard.innerHTML = buttons;
      }

      // register each button's click event
      Array.from(eleGameBoard.children).forEach(function(button, index) {
        // set id to each button for easy retriever later
        button.id = 'tictactoe-button-' + index;

        // save button to local variable
        if( ! this._buttons) {
          this._buttons = []; // not exist, so create button array
        }
        this._buttons.push(button); // add this button to array

        // set 'click' event
        this.replaceEventListener(button, 'click', this.set.bind(this, this.CHARS[0], index));
      }.bind(this));

      this.btnGameStartHuman = document.getElementById(gameStartHumanBtnId);
      this.replaceEventListener(this.btnGameStartHuman, 'click', this.gameStart.bind(this, this.CHARS[0]));

      this.btnGameStartComputer = document.getElementById(gameStartComputerBtnId);
      this.replaceEventListener(this.btnGameStartComputer, 'click', this.gameStart.bind(this, this.CHARS[1]));

      this.btnGameStop = document.getElementById(gameStopBtnId);
      this.replaceEventListener(this.btnGameStop, 'click', this.gameStop.bind(this));

      this.labelGameMessage = document.getElementById(gameMessageLableId);

      this.useKnockOut = false;

      // overwrite 3 previous pre-define 'Model' data
      // from KnockOut 'observerable' to be vanilla JS functions
      this.playing = function(playing) {
        if(playing == undefined) {
          // no parameter means get value
          return this._isPlaying;
        }
        this._isPlaying = playing;

        if(this.btnGameStartHuman && this.btnGameStartComputer && this.btnGameStop) {
          this.btnGameStartHuman.style.display = this._isPlaying ? 'none' : 'unset';
          this.btnGameStartComputer.style.display = this._isPlaying ? 'none' : 'unset';
          this.btnGameStop.style.display = this._isPlaying ? 'unset' : 'none';
        }
      };
      this.gameMessage = function(text) {
        if(text == undefined) {
          // no parameter means get value
          return this._gameMessage;
        }
        this._gameMessage = text;

        if(this.labelGameMessage) {
          this.labelGameMessage.innerHTML = text;
        }
      }
      this.characterPositions = function(characters) {
        if(characters == undefined) {
          // no parameter = get value
          return this._characterPositions;
        }
        this._characterPositions = characters;

        // update buttons if exist
        if(this._buttons && this._buttons.length == characters.length) {
          characters.forEach((charater, position) => this._buttons[position].innerHTML = charater);
        }
      }

      // reset game
      this.resetGame(this.firstTimeOnlyData);
    }

    TicTacToe.prototype.replaceEventListener = function(element, event, listenerToAdd, listenerToRemove) {
      // remove previous listener (if exist)
      if(listenerToRemove) {
        element.removeEventListener(event, listenerToRemove);
      }

      // add
      if(listenerToAdd) {
        element.addEventListener(event, listenerToAdd);
      }
    }

    gameViewModel.initTicTacToeWithoutKnockOut('tictactoe-game-board', 'btn-start-human', 'btn-start-computer', 'btn-game-stop', 'label-game-message');
  }
}

We have created a function prototype 'initTicTacToeWithoutKnockOut' to parse all UI elements (buttons and text label) in this game demo.

Without KnockOutJS we can not use 'observable', therefore we must change these 3 variables (playing, gameMessage and characterPositions) to methods, if any of these methods is called with parameter value such as gameMessage('You win') then the text 'You win' will be saved and any binded UI elements will be updated, but if called without parameter then it will only get the saved value (eg: 'var text = gameMessage()').

Please note this is only simple demo for learning purpose, KnockOutJS library is not necessary for this demo but I use it to show you the code comparison with and without a library, the vanilla JavaScript code is not optimized and could be better.

KnockOut is smaller compare to other more features 'high-end' front-end libraries (AngularJS, ReactJS, VueJS), there is smaller library called BackboneJS, but I found BackboneJS is not suitable for a beginner because too small (v1.4.0 is only 7.9KB packed and gzipped) and need other dependencies.

What great idea do you have for MVVM? let's make Single-Page-Application (SPA) website!

Korean translation

이 게임 데모에서 모든 UI 요소 (버튼 및 텍스트 라벨)를 바인딩하기 위해 'initTicTacToeWithoutKnockOut' 이라는 함수 prototype이 생성됩니다.

KnockOutJS를 사용하지 않기 때문에 'observable'가 없습니다. 따라서 이 세 변수(playing, gameMessage 및 characterPositions)를 메소드로 변경해야합니다.

이 데모에서 만약에 메서드를 호출하는데 parameter(매개 변수) 있을때 (예: gameMessage('You win')) 'You win'텍스트가 저장되고 바인딩 된 UI 요소가 업데이트됩니다, 매개 변수없이 호출 된 경우 저장된 값만을 가져옵니다 (예: 'var text = gameMessage()').

이 코드는 학습 목적을위한 간단한 데모이며 코드가 최적화되지 않았으므로 더 좋을 수 있습니다.

KnockOut은 다른 '고급' front-end 라이브러리 (AngularJS, ReactJS, VueJS)와 비교하여 작습니다. KnockOut보다 BackboneJS 라는 라이브러리가 있지만, 너무 작아서 (v1.4.0이 7.9KB 만 압축되고 gzip 됨) 다른 종속성이 필요하기 때문에 BackboneJS가 초보자에게 적합하지 않음을 발견했습니다.

MVVM 패턴에 대한 훌륭한 아이디어가 있을까요? Single-Page-Application website을 만들어 봅시다!