Skip to content

Instantly share code, notes, and snippets.

@justinbmeyer
Last active October 11, 2018 00:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justinbmeyer/ec767e11d79f092432ac1eac310dc37f to your computer and use it in GitHub Desktop.
Save justinbmeyer/ec767e11d79f092432ac1eac310dc37f to your computer and use it in GitHub Desktop.
A CanJS debugging tutorial. Can you solve the riddles?

Outline

Setup

Problem

  • In this section, we will make sure that we have access to the debugging in various environments:
    • Using the mjs builds (CodePen).
    • Using StealJS individual builds.

We want to be able to get this component working:

Component.extend({
    tag: "my-counter",
    view: `
        Count: <span>{{this.count}}</span>
        <button on:click='this.increment()'>+1</button>
    `,
    ViewModel: {
        count: {default: 0},
        increment() {
            this.count++;
        }
    }
});

And then be able to see its ViewModel in the CanJS ViewModel inspector:

image

We also want to be able to write can.debug and access the debugger helpers:

image

What you need to know

Chrome Dev Tools

CodePen

  • Start a new CodePen
  • Import Component from "https://unpkg.com/can/core.mjs".
  • Make sure to add <my-counter></my-counter> to the HTML.

Steal

mkdir can-steal
cd can-steal
npm init --yes
npm i steal can-component can-debug
npm i steal-conditional --save-dev

Configure steal-conditional in package.json

{
  "name": "can-steal",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "can-component": "^4.4.2",
    "can-debug": "^2.0.1",
    "steal": "^2.1.3",
    "steal-conditional": "^1.1.1"
  },
  "steal": {
    "configDependencies": [
      "node_modules/steal-conditional/conditional"
    ]
  }
}

dev.html

<my-counter></my-counter>
<script src="./node_modules/steal/steal.js" main></script>

index.js

import Component from "can-component";

import debug from "can-debug#?./is-dev";
debug();

Component.extend({
    tag: "my-counter",
    view: `
        Count: <span>{{this.count}}</span>
        <button on:click='this.increment()'>+1</button>
    `,
    ViewModel: {
        count: {default: 0},
        increment() {
            this.count++;
        }
    }
});

is-dev.js

import steal from "@steal";
export default !steal.isEnv("production")

Solution

Log when a property or properties change with .log

The problem

This CodePen changes confusing's properties. The property names write out a message.

What you need to know

Use .log() to log when any property changes on an observable. Use .log(key) to log when a specific property changes on an observable.

The solution

Click to See

Add the following after confusing is instantiated:

// Maybe listen to properties changing here:
confusing.log();

The properties changed write out all you need is love.

Break when a property changes

The problem

We want to see what's changing a property.

This CodePen is randomly changing propA, propB, propC. When those change, final is being incremented:

confusing.on("ticker", function(){
  var props = ["propA","propB","propC"];
  var prop = props[rand()];
  confusing[prop]++;
},"domUI");
confusing.on("propA", function canjs(){
  confusing.final++;
},"domUI");
confusing.on("propB", function stealjs(){
  confusing.final++;
},"domUI");
confusing.on("propC", function donejs(){
  confusing.final++;
},"domUI");

Which function (canjs, stealjs, or donejs) is the one that is called when final is incremented to 5?

What you need to know

If you simply listen to when a property changes like:

map.on("key", function(ev, newVal){
  if(newVal === 5) {
    debugger;
  }
});

That handler will not be called immediately upon the change of the property. Instead, that handler will be added to the mutate queue which fires at the end of a batch of changes.

If we do this with the CodePen:

confusing.on("final", function(ev, newVal){
  if(newVal === 5) {
    debugger;
  }
});

We see this:

image

Instead, if you want to be immediately notified of a change, listen to the event in the "notify" phase as follows:

map.on("prop", function handler(){
  debugger;
}, "notify");

The solution

Click to See

Add the following to the CodePen:

confusing.on("final", function(ev, newVal){
  if(newVal === 5) {
    debugger;
  }
},"notify");

The answer is stealjs.

image

Understand what caused a particular thing to happen with logStack

The problem

Figure out the properties that result in the final property changing in the following CodePen.

What you need to know

  • The call stack only tells you the first observable to change. Everything else you see are queues functions: image

  • can-queues maintains a stack trace everything it does. CanJS does its best to give descriptive names to what is happening.

  • can.queues.logStack() prints this stack.

The solution

Click to See

The properties changed spell s t a c k.

image

Understand what caused a particular thing to happen with logStack's reasonLog

The problem

While debugging the final property changing in the following CodePen, you want to know what changed in message to cause the final change.

What you need to know

logStack entries are really just function calls. Each entry gets logged with an object that incldues:

  • args - The arguments passed to the function
  • context - The this of the function
  • fn - The function that was called
  • meta - Additional information queues uses for debugging.

Critically, the meta object also includes a reasonLog. This is indented to be a human-readable explination of why that task was queued. CanJS provides it in development mode on most tasks.

image

The Solution

Click to See

The property changed to "reason" from "log":

image

Logging the state of the ViewModel

The problem

There's a <some-state> component on the page in this CodePen. Log its viewModel's properties and values.

What you need to konw

Components elements now have their viewModel available as element.viewModel. So use:

document.querySelector("some-component").viewModel

To get the ViewModel and:

document.querySelector("some-component").viewModel.get()

To see it in an object form.

If you inspect the element, you can also use $0 to reference the last element you inspected:

$0.viewModel.get()

The solution

Click to See

Add:

console.log(document.querySelector('some-state').viewModel.get())

And you should see logged:

{
  a: "viewModel",
  property: "makes",
  sense: "right"
}

Understanding what caused something to change with components and logStack

The problem

  • Understanding how component bindings (foo:bind="bar") resulted in property updates can be confusing.
  • This CodePen's <word-and>'s you property is changing as a result of several child components of <my-app> passing around the value.
  • Can you trace how the value moved from one property to the next?
  • Record the word in each "word" component's name and the property that was changed and it will spell a message.
    • For example, if a component like <word-hello>'s world property changed, you would record "hello world".
  • There are 4 sub components whose properties changed.

What you need to know

When a binding updates a value, an entry like the following is added to the queue:

DOM_UI ran task: <a-component viewModelProp:bind="scopeKey"> updates <a-component>.viewModelProp from {{scopeKey}}

This means that scopeKey changed and <a-component>.viewModelProp was set to its value.

Bindings can also run the other way, so you might see:

DOM_UI ran task: <a-component viewModelProp:bind="scopeKey"> updates {{scopeKey}} from <a-component>.viewModelProp

This means <a-component>.viewModelProp changed and scopeKey was set to its value.

The solution

Click to See

The properties are updated as can.js loves.javascript debugging.tools and.you:

image

Logging the scope

The problem

This Codepen has a class with a student with a missing parent name. Can you figure out which class and student has the missing parent name by exploring the scope?

What you need to know

You can call scope.log() to log stache's scope.

If you don't want to do it all the time, it helps to do it conditionally:

{{# if(logTheScope) }} {{ scope.log() }}  {{/if}}

HINT: {{^ if() }} can be used to inverse logic.

The solution

Click to See

Conditionally call scope.log():

          {{# for(parent of student.parents) }}
            {{^ if(parent.name) }} {{scope.log()}} {{/ if }}
            <li>{{parent.name}}</li>
          {{ /for}}

Then exploring the result will show the class is math and the student is Rahim:

image

Logging values in the scope

The problem

This Codepen has a class with a student with a missing parent name. Can you figure out which class and student has the missing parent name by logging values in the scope?

What you need to know

You can use {{ console.log(key) }} to log values in the scope.

The solution

Click to See

Use console.log():

          {{# for(parent of student.parents) }}
            {{console.log(class.name, student.name, parent.name)}}
            <li>{{parent.name}}</li>
          {{ /for}}

Then exploring the result will show the class is math and the student is Rahim:

image

Debugging the scope

The problem

This Codepen has a class with a student with a missing (undefined) parent name. Can you figure out which class and student has the missing parent name by debugging the scope?

The CodePen uses the global build. Click to find out why.

There is a bug in the .mjs builds. scope and the get function are being dead code eliminated.

To read something from the scope:

arguments[2].scope.get("class").name

What you need to know

Break anytime this part of the template evaluates
{{debugger()}}

Break when condition is truthy
{{debugger(condition)}}

Break when left equals right
{{debugger(left, right)}}

This will break and give you access to a get function that reads from the scope like:

get("class") //-> DefineMap{}

The scope itself is available as options.scope:

options.scope.get("class") //-> DefineMap{}

PROTIP: If you have stacheConverters included, you could use not() like:

{{ debugger( not(key) ) }}

The Solution

Click to See

Use debugger(parent.name, undefined):

          {{# for(parent of student.parents) }}
            {{debugger(parent.name, undefined)}}
            <li>{{parent.name}}</li>
          {{ /for}}

Then exploring the result will show the class is reading and the student is Adisa:

image

Understand what changes the DOM

The problem

This CodePen has an <h2> element that reads a whatChangesMe element like:

<h2>What Changes Me? {{this.whatChangesMe}}</h2>

Can you figure out what properties of the <my-app> ViewModel update the <h2> element?

What you need to know

Use can.debug.logWhatChangesMe(element) to log what changes an HTML element:

can.debug.logWhatChangesMe(element)

The solution

Click to See

Inspect the <h2> element and run the following in the console:

can.debug.logWhatChangesMe($0)

image

Understand what changes an observable

The Problem

This CodePen has a <my-app>'s element with a first and last property. One of the 6 inputs changes the first property and one of the 6 inputs changes the last property. Can you discover those inputs without changing the inputs?

What you need to know

can.debug.logWhatChangesMe(observable [,key] ) will list out all the values (including elements) that change a value.

The Solution

Click to See
can.debug.logWhatChangesMe($0.viewModel, "first")
can.debug.logWhatChangesMe($0.viewModel, "last")

First is changed by Thomas.

Last is changed by Paula.

@mikemitchel
Copy link

It would be nice if the CodePen's opened in a new tab by default

@mickmcgrath13
Copy link

For: https://gist.github.com/justinbmeyer/ec767e11d79f092432ac1eac310dc37f#understand-what-caused-a-particular-thing-to-happen-with-logstacks-reasonlog
Maybe use something like:

["Observation<Confusing{}'s message getter>", "changed to", "bar", "from", "foo"]

instead of

["Observation<Confusing{}'s message getter>", "changed to", "reason", "from", "log"]

there are already enough reasons and logs floating around, and the 'answer' is a bit ambiguous.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment