Search code examples
javascripthtmlarraysjsonknockout.js

How to read Json data using Knockout?


I want to create a Typewriter Effect using knockout, but I cannot store the data inside my table in order to create said effect. I've done some research on for a few weeks and I don't have the fundamental knowledge to stumble my way through this issue. This is my first time asking on Stack overflow so bare with me.


This is based off of this codipen: https://codepen.io/drewdoesdev/pen/gVrOBm

Tried this for typewriter codipen: https://codepen.io/sauranerd/pen/mKKXjV?editors=1111


This example above doesn't help with retrieving data and

Here's a short version of my variables:

var story = {
    0: {
      text: "zzzzz", 
      image: "img/gargoylep.gif",
      choices: [
        {
             choiceText: "wake them?",//this is a button that goes to the next story
             storyLink: 1
             
        },
      ]
    },
    1: {
      image: "img/gargoylesk.gif",
      text: "im mad",
      choices: [
        {
             choiceText: "Continue",
             storyLink: 2
        }
      ]
    },
}

Below this is what displays the text. The way this works is It starts the game, goes to 0 to display text in order, then changes scene (story link) to the next one. It's very similar to the "dinner with your dad" Codipen above.

<span class="scene__text" data-bind="text: gameText"></span>

 <button class="scene__choice" data-bind="text: choiceText, click: changeScene">Click Here to Start</button>

I don't want to do this effect with css as I want to control the speed of the type writer. I have code I could use if I could store the string somehow.

Let me know if I need to show more code, or explain further. An example of working code would help me the most. I assume I am just totally missing the syntax for how these work. I appreciate the help!

I have followed the knockout Documentation, and read up on w3 about json. Kept getting "Object Object" as a result with their examples. Not sure if I could somehow retrieve the text that's displaying and then send it to a typewriting function I wrote.

Edit: To clarify, I want to assign multiple functions for the typing speed (aka fast, slow normal) to each unique text.


Solution

  • You could create a custom binding that injects text using the typewriter effect.

    For example, the typeWriter binding below:

    • Takes any text value, either observable or not.
    • Creates a new observable text that contains a substring based on the effect's progress. It starts with "" and ends with the original string.
    • Applies the default text binding to this new value
    • Sets up an animation of a dynamic duration (10ms per character in the string)
      • On every frame, the elapsed time is used to calculate the correct intermediate state
      • Knockout syncs the intermediate state to the DOM

    If you want to make the duration dynamic, you can use the allBindings parameter to pass additional settings (see the first example in the documentation)

    const texts = [
      "Something short.",
      "Medium length, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud",
      "A longer paragraph: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    ];
    
    ko.bindingHandlers.typeWriter = {
      init: (el, va) => {
        const partialText = ko.observable("");
        let animation;
    
        const effect = ko.computed(() => {
          const t0 = Date.now();
          const fullText = ko.unwrap(va());
          const duration = fullText.length * 10; // 10ms per char
    
          animation = requestAnimationFrame(next);
    
          function next() {
            const p = (Date.now() - t0) / duration;
            if (p >= 1) {
              partialText(fullText);
            }
    
            partialText(
              fullText.slice(0, Math.round(p * fullText.length))
            );
    
            animation = requestAnimationFrame(next);
          }
        });
        
        ko.applyBindingsToNode(el, { text: partialText });
    
        ko.utils.domNodeDisposal.addDisposeCallback(el, () => {
          cancelAnimationFrame(animation);
          effect.dispose();
        });
      }
    }
    
    
    const activeIdx = ko.observable(0);
    const activeText = ko.pureComputed(() => texts[activeIdx()]);
    const goNext = () => activeIdx((activeIdx() + 1) % texts.length);
    ko.applyBindings({ activeText, goNext });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    
    <button data-bind="click: goNext">Next</button>
    
    <h2>Typewriter text binding</h2>
    <p data-bind="typeWriter: activeText"></p>

    Here's an example that uses your model object: (Note, just for fun I asked ChatGPT to write an adventure for us :D )

    // Set up view model
    const activeStoryId = ko.observable(1)
      // This forces a re-render if we ever want to loop back to the current story
      .extend({ notify: "always" });
      
    const activeStory = ko.pureComputed(() => 
      story[activeStoryId()] ?? 
      // Default to "The end" so user can restart
      { text: "The end", choices: [{ choiceText: "Back to start", storyLink: 1 }] }
    );
    
    const onChoice = ({ storyLink }) => activeStoryId(storyLink);
    
    // Apply bindings
    registerBinding();
    ko.applyBindings({
      onChoice,
      activeStory
    });
    
    function registerBinding() {
      ko.bindingHandlers.typeWriter = {
        init: (el, va) => {
          const partialText = ko.observable("");
          let animation;
    
          const effect = ko.computed(() => {
            const t0 = Date.now();
            const fullText = ko.unwrap(va());
            const duration = fullText.length * 30; // 10ms per char
    
            animation = requestAnimationFrame(next);
    
            function next() {
              const p = (Date.now() - t0) / duration;
              if (p >= 1) {
                partialText(fullText);
              }
    
              partialText(
                fullText.slice(0, Math.round(p * fullText.length))
              );
    
              animation = requestAnimationFrame(next);
            }
          });
    
          ko.applyBindingsToNode(el, {
            text: partialText
          });
    
          ko.utils.domNodeDisposal.addDisposeCallback(el, () => {
            cancelAnimationFrame(animation);
            effect.dispose();
          });
        }
      }
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
    <script>
      const story = {"1":{"text":"You are standing in front of a big castle. What do you do?","choices":[{"choiceText":"Enter the castle","storyLink":"2"},{"choiceText":"Walk around the castle","storyLink":"3"}]},"2":{"text":"You are inside the castle. You see two doors. Which one do you choose?","choices":[{"choiceText":"Left door","storyLink":"4"},{"choiceText":"Right door","storyLink":"5"}]},"3":{"text":"You are walking around the castle. Suddenly, you are attacked by a monster. What do you do?","choices":[{"choiceText":"Fight the monster","storyLink":"6"},{"choiceText":"Run away","storyLink":"7"}]},"4":{"text":"You find a treasure chest. What do you do?","choices":[{"choiceText":"Open the chest","storyLink":"8"},{"choiceText":"Leave the chest and continue","storyLink":"9"}]},"5":{"text":"You find yourself in a room with no way out. What do you do?","choices":[{"choiceText":"Search for a hidden passage","storyLink":"10"},{"choiceText":"Scream for help","storyLink":"7"}]},"6":{"text":"You fought bravely, but the monster was too strong. You died.","choices":[]},"7":{"text":"You run away and find yourself in front of the castle. What do you do?","choices":[{"choiceText":"Enter the castle","storyLink":"2"},{"choiceText":"Walk around the castle","storyLink":"3"}]},"8":{"text":"You found a magical sword inside the chest. What do you do?","choices":[{"choiceText":"Leave the castle with the sword","storyLink":"11"},{"choiceText":"Continue exploring the castle","storyLink":"2"}]},"9":{"text":"You continue exploring the castle and find a secret passage. What do you do?","choices":[{"choiceText":"Enter the passage","storyLink":"12"},{"choiceText":"Leave the passage and continue","storyLink":"5"}]},"10":{"text":"You found a hidden passage and escaped from the room. What do you do?","choices":[{"choiceText":"Continue exploring the castle","storyLink":"2"},{"choiceText":"Leave the castle","storyLink":"11"}]}}
    </script>
    
    <div data-bind="with: activeStory">
      <p data-bind="typeWriter: text" style="min-height: 50px"></p>
      
      <!-- ko foreach: choices -->
      <button data-bind="click: $root.onChoice, text: choiceText"></button>
      <!-- /ko -->
    </div>