DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Migrating to CanJS 6
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Logic

  • Edit on GitHub

Learn how to write observables in an organized, maintainable, and testable way.

This guide will show you many techniques for writing the logic of an element props so that it is easy to test and easy to maintain. It will show how to organize props so that there is a clear purpose for each property and method, how to use derived properties to handle complex logic, how to write methods that are focused on doing a single thing, how to isolate side effects so they do not affect other logic and unit tests, and how to clean up event listeners to prevent memory leaks.

Organize element props

The organization of an element props should convey which properties should change and where those changes should happen. We suggest organizing props using the following sections:

  • External stateful properties - Properties passed in to the component through bindings.
  • Internal stateful properties - Stateful properties "owned" by this component.
  • Derived properties - Properties derived from stateful properties.
  • Methods - Methods that change stateful properties and dispatch events that can be used in derived properties and side effects.
  • Side effects - Side effects that depend on the DOM should be done in the connected lifecycle hook.

Here is an example (click the "Run in your browser" button to see them in action):

<props-organization heading:raw="Props Organization Example"></props-organization>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script type="module">
import { StacheElement, type } from "can";

class PropsOrganization extends StacheElement {
  static view = `
    {{# if(this.heading) }}
      <h2>{{ this.heading }}</h2>
    {{/ if }}

    <p>
      {{# if(this.showExternalStatefulProperties) }}
        <span on:click="this.showExternalStatefulProperties = false">-</span>
      {{ else }}
        <span on:click="this.showExternalStatefulProperties = true">+</span>
      {{/ if }}
      External stateful properties
    </p>
    <p {{# unless(this.showExternalStatefulProperties) }}class="hidden"{{/ unless }}>
      Properties passed in to the component through <a href="//canjs.com/doc/can-stache-bindings.html">bindings</a>.
    </p>

    <p>
      {{# if(this.showInternalStatefulProperties) }}
        <span on:click="this.showInternalStatefulProperties = false">-</span>
      {{ else }}
        <span on:click="this.showInternalStatefulProperties = true">+</span>
      {{/ if }}
      Internal stateful properties
    </p>
    <p {{# unless(this.showInternalStatefulProperties) }}class="hidden"{{/ unless }}>
      Stateful properties "owned" by this component.
    </p>

    <p>
      {{# if(this.showDerivedProperties) }}
        <span on:click="this.hideDerivedProperties = true">-</span>
      {{ else }}
        <span on:click="this.hideDerivedProperties = false">+</span>
      {{/ if }}
      Derived properties
    </p>
    <p {{# unless(this.showDerivedProperties) }}class="hidden"{{/ unless }}>
      Properties derived from stateful properties. This is where most of the logic of props should happen.
    </p>

    <p>
      <button on:click="this.toggleShowMethods()">
        {{# if(this.showMethods) }}Hide {{/ if }} Methods
      </button>
    </p>
    <p {{# unless(this.showMethods) }}class="hidden"{{/ unless }}>
      Methods can change stateful properties and dispatch events that can be used in derived properties and side effects.
    </p>

    <p><button on:click="this.showSideEffects()">Side Effects</button></p>
    <div class="modal" tabindex="-1" role="dialog">
      <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
          <div class="modal-body">
            <p>The connected lifecycle hook is where side effects should be handled. This will prevent them from happening during unit tests of the props.</p>
          </div>
        </div>
      </div>
    </div>
  `;

  static props = {
    // EXTERNAL STATEFUL PROPERTIES
    heading: type.maybeConvert(String),

    // INTERNAL STATEFUL PROPERTIES
    showExternalStatefulProperties: false,

    showInternalStatefulProperties: false,
    hideDerivedProperties: true,
    showMethods: false,

    // DERIVED PROPERTIES
    get showDerivedProperties () {
      return !this.hideDerivedProperties;
    }
  };

  toggleShowMethods() {
    this.showMethods = !this.showMethods;
  }

  showSideEffects() {
    this.dispatch("show-side-effects");
  }

  connected() {
    const $modal = $(this).find(".modal");

    this.listenTo("show-side-effects", () => {
      $modal.show();
    });

    this.listenTo(window, "click", () => {
      $modal.hide();
    });
  }
};
customElements.define("props-organization", PropsOrganization);
</script>
<style>
props-organization {
  display: block;
}

span {
  cursor: pointer;
  font-weight: 700;
  padding: 4px;
}

span.right-arrow:after {
 content: "►";
}

span.down-arrow:after {
  content: "▼";
}

p.hidden {
  display: none;
}
</style>

Derived properties

A derived property is a property whose value is not set by any other function; derived properties calculate their own value based on the values of other properties, changes to other properties, and other events.

Since the logic of a derived property is isolated to that property’s definition, it is much easier to understand and debug than a property that can be changed directly by other functions. Beacuse of this, most of the logic of an elements props should be handled by derived properties.

CanJS has two ways to create derived properties:

  • get property behaviors can derive their value from the value of other properties and the value they were last set to.
  • value property behaviors can derive their value from more complex logic such as changes to other properties, dispatched events, and their own previously derived values.

The sections below show how to use these property behaviors to build maintainable, testable props.

Derive properties from other properties

Getters in CanJS can be used to derive a value based on the current value of the properties read by the getter, each time the getter runs.

Getters will run when the property is read, and automatically re-run whenever one of the properties read by the getter changes (if the property defined by the getter is bound).

Getters can be used to replace imperative logic like this:

<a-pp></a-pp>
<script type="module">
import { StacheElement } from "can";

class App extends StacheElement {
  static view = `
    <p>First: <input value:from="this.first" on:change="setFirst(scope.element.value)"></p>
    <p>Last: <input value:from="this.last" on:change="setLast(scope.element.value)"></p>
    <p>{{ this.name }}</p>
  `;

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    first: "Kevin",

    last: "McCallister",
    name: "Kevin McCallister"
  };

  setFirst(first) {
      this.first = first;
      this.name = `${first} ${this.last}`;
  }

  setLast(last) {
      this.last = last;
      this.name = `${this.first} ${last}`;
  }
};
customElements.define("a-pp", App);
</script>

Using a derived property removes a lot of this boilerplate, and more importantly, isolates the logic for name to name’s property definiton.

<a-pp></a-pp>
<script type="module">
import { StacheElement } from "can";

class App extends StacheElement {
  static view = `
    <p>First: <input value:bind="this.first"></p>
    <p>Last: <input value:bind="this.last"></p>
    <p>{{ this.name }}</p>
  `;

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    first: "Kevin",
    last: "McCallister",

    // DERIVED PROPERTIES
    get name() {
      return `${this.first} ${this.last}`;
    }
  };
};
customElements.define("a-pp", App);
</script>

Side effects when derived properties change

When using derived properties like this, it can be tempting to add side effects inside of the getter. For example, if you wanted to keep a list of all of the values of name over time, you might try to do something like this:

static props = {
  // INTERNAL STATEFUL PROPERTIES
  first: "Kevin",
  last: "McCallister",
  names: {
    get default() {
      return new DefineList();
    }
  },

  // DERIVED PROPERTIES
  get name() {
    const name = `${this.first} ${this.last}`;
    this.names.push( name );
    return name;
  }
}

Doing mutations like this can cause lots of problems, including inifinite loops and stack overflows.

Mutations should not be done in a getter.

Here is an example demonstrating the issues that can be caused by doing mutations in a getter:

<a-pp></a-pp>
<script type="module">
import { ajax, fixture, StacheElement } from "can";

fixture.delay = 500;

fixture({ url: "/api/{id}" }, (req, resp) => {
  const id = req.data.id;

  if (id < 50) {
    resp({ data: { id: id } });
  } else {
    resp(404, { message: "Not Found" });
  }
});

class App extends StacheElement {
  static view = `
    {{# if(this.data.error) }}
      <p class="error">Error: {{ this.data.error }}</p>
    {{ else }}
      <p>{{ this.data.id }}</p>
    {{/ if }}
  `;

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    id: 0,

    // DERIVED PROPERTIES
    data: {
      async(resolve, lastSet) {
        ajax({
          url: `/api/${this.id}`
        })
        .then(resp => {
          resolve(resp.data);
          this.id++;
        })
        .catch(err => {
          resolve({
            error: err.message
          })
        });
      }
    }
  };
};
customElements.define("a-pp", App);
</script>

This example uses an async getter to make an ajax call to retrieve data for the current id. Once the response is received, the getter uses resolve to set the value of the property then increments id. Since this getter also reads id to create the URL for the ajax request, the getter immediately re-runs when id changes. This means that the getter will continue making ajax requests in an infinite loop.

In real-world applications, where a getter may depend on other derived values and have many dependencies, it can be much less obvious that this type of circular dependency exists. This is why mutations should not be done in a getter.

The next section will show how to handle situations like this where you need to update a derived value every time another property changes.

Derive properties from changes to another property

The value property behavior is available for defining complex derived values that cannot be created using getters.

The value property behavior defines a property by listening to changes in other properties (and its own last set value) and calling resolve with new values.

Here is an example of using value to keep a list of all of the values of a derived name property over time:

<a-pp></a-pp>
<script type="module">
import { ObservableArray, StacheElement } from "can";

class App extends StacheElement {
  static view = `
    <p>{{ this.name }}</p>

    <h2>Names:</h2>
    {{# for(name of this.names) }}
      <p>{{ name }}</p>
    {{/ for }}
  `;

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    first: "Kevin",
    last: "McCallister",

    // DERIVED PROPERTIES
    get name() {
      return `${this.first} ${this.last}`;
    },
    names: {
      value({ listenTo, resolve }) {
        let names = resolve( new ObservableArray([ this.name ]) );

        listenTo("name", (ev, name) => {
          names.push(name);
        });
      }
    }
  };
};
customElements.define("a-pp", App);
</script>

Using a value defition, allows names to update when name changes and eliminates having the need for the side effect in the name getter. This makes it much easier to debug the names property and makes this code much easier to maintain.

Derive properties from changes to multiple other properties

The value property behavior can also be quite useful when a property needs to derive its value from changes to multiple other properties. The value function can listen to changes in all of the necessary properties and resolve with the new value accordingly.

The following example has a slider for selecting a value within a range. The value should be updated:

  • when the user selects a new value using the slider, set the selectedValue property to the new value or to the minimum or maximum if the new selectedValue is outside the range
  • if the minimum changes, set the selectedValue property to the new minimum if it is currently lower
  • if the maximum changes, set the selectedValue property to the new maximum if it is currently higher

This could be accomplished using a series of setters like in the code below. However, this scatters the mutations for selectedValue throughout the props:

  // INTERNAL STATEFUL PROPERTIES
  selectedValue: {
    default: 100,
    set(val) {
      return val > this.maximum ? this.maximum :
              val < this.minimum ? this.minimum : val;
    }
  },
  minimum: {
    default: 50,
    set(min) {
      if (this.selectedValue < min) {
        this.selectedValue = min;
      }
      return min;
    }
  },
  maximum: {
    default: 150,
    set(max) {
      if (this.selectedValue > max) {
        this.selectedValue = max;
      }
      return max;
    }
  }

Code like this becomes very hard to maintain as you cannot look at a the property definition for selectedValue to understand how it works.

Keeping all of the logic for a property within its property definition makes code much easier to maintain.

This same functionality can be accomplished using a value definition:

<range-slider></range-slider>
<script type="module">
import { StacheElement } from "can";

class RangeSlider extends StacheElement {
  static view = `
    <p>
      Min: <input type="range" min="0" max="200" valueAsNumber:bind="this.minimum"> {{ this.minimum }}
    </p>

    <p>
    Value: <input type="range" min="0" max="200" valueAsNumber:bind="this.selectedValue"> {{ this.selectedValue }}
    </p>

    <p>
      Max: <input type="range" min="0" max="200" valueAsNumber:bind="this.maximum"> {{ this.maximum }}
    </p>
  `;

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    minimum: 50,
    maximum: 150,

    // DERIVED PROPERTIES
    selectedValue: {
      value({ listenTo, lastSet, resolve }) {
        let latest = resolve(100);

        listenTo(lastSet, (val) => {
          latest = val;

          if (latest > this.maximum) {
            latest = this.maximum;
          }

          if (latest < this.minimum) {
            latest = this.minimum;
          }

          resolve(latest);
        });

        listenTo("minimum", (ev, min) => {
          if(latest < min) {
            resolve(min);
          }
        });

        listenTo("maximum", (ev, max) => {
          if(latest > max) {
            resolve(max);
          }
        });
      }
    }
  };
};
customElements.define("range-slider", RangeSlider);
</script>

Methods

Methods are a staple of Object Oriented Programming. In CanJS applications, methods can be used to update props state when an event occurs.

Use a method to set a property

Methods can be used to update stateful properties when an event occurs:

<a-pp></a-pp>
<script type="module">
import { StacheElement } from "can";

class App extends StacheElement {
  static view = `
    <p>{{ this.day }}</p>

    <button on:click="this.resetDay()">Reset Day</button>
  `;

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    day: "Sun"
  };

  resetDay() {
    this.day = "Sun";
  }
};
customElements.define("a-pp", App);
</script>

You can also do this using a "simple setter" directly in the view:

  static view = `
    <p>{{ this.day }}</p>

    <button on:click="this.day = 'Sun'">Reset Day</button>
  `

To simplify debugging and make documentation easier, using methods may still be useful for your application.

Use a method to set multiple properties

If you need to do update more than one stateful property when an event occurs, it can be tempting to generalize the event handler to do everything:

  static view = `
    {{# if(this.editing) }}
      <input value:bind="this.day">
    {{ else }}
      <p>{{ this.day }}</p>
    {{/ if }}

    <button on:click="this.resetDay()">Reset Day</button>
  `,

  static props = {
    // INTERNAL STATEFUL PROPERTIES
    day: "Sun",
    editing: true,

    // METHODS
    resetDay() {
      this.day = "Sun";
      this.editing = false;
    }
  }

Functions like this become very hard to maintain as an application continues to grow because it is difficult to understand everything that will happen when the function is called.

Instead of using a single function to update multiple properties, dispatch can be used to dispatch an event that other properties can then derive their values from:

<a-pp day:raw="Wed" editing:raw="true"></a-pp>
<script type="module">
import { StacheElement } from "can";

class App extends StacheElement {
  static view = `
    {{# if(this.editing) }}
      <input value:bind="this.day">
    {{ else }}
      <p>{{ this.day }}</p>
    {{/ if }}

    <button on:click="this.resetDay()">Reset Day</button>
  `;

  static props = {
    // DERIVED PROPERTIES
    day: {
      default: "Sun",
      value({ lastSet, listenTo, resolve }) {
        resolve( lastSet.value );

        listenTo("reset-day", () => {
          resolve("Sun");
        });
      }
    },

    editing: {
      default: false,
      value({ lastSet, listenTo, resolve }) {
        resolve( lastSet.value );

        listenTo("reset-day", () => {
          resolve(false);
        });
      }
    }
  };

  resetDay() {
    this.dispatch("reset-day");
  }
};
customElements.define("a-pp", App);
</script>
<style>
input {
  width: 100px;
}

p {
  width: 100px;
  margin: 0;
  display: inline-block;
}
</style>

Using this technique, it is possible to read each property definition and know exactly how it will behave when this event occurs.

DOM side effects

Side effects of properties changing that depend on the DOM being available should be handled in the connect. This will ensure that the props can be tested without the DOM.

The connected lifecycle hook can use listenTo to listen to dispatched events or property changes and perform necessary side effects. This example shows how to play an <audio> element each time a button is pressed:

<memory-game></memory-game>
<script type="module">
import { ObservableArray, StacheElement, type } from "//unpkg.com/can@5/core.mjs";

class ColorPicker extends StacheElement {
  static view = `
    <button
      on:click="this.selectColor( this.color )"
      style="background-color: {{ this.color }}"
    ></button>
  `;

  static props = {
      // EXTERNAL STATEFUL PROPERTIES
      color: type.maybeConvert(String)
    };
  }
};
customElements.define("color-picker", ColorPicker);

class Simon extends StacheElement {
  static view = `
    <audio
      class="green"
      src="https://s3.amazonaws.com/freecodecamp/simonSound1.mp3"
    ></audio>
    <audio
      class="red"
      src="https://s3.amazonaws.com/freecodecamp/simonSound2.mp3"
    ></audio>
    <audio
      class="yellow"
      src="https://s3.amazonaws.com/freecodecamp/simonSound3.mp3"
    ></audio>
    <audio
      class="blue"
      src="https://s3.amazonaws.com/freecodecamp/simonSound4.mp3"
    ></audio>

    <div>
      <color-picker
        selectColor:from="this.selectColor"
        color:raw="green"
      ></color-picker>

      <color-picker
        selectColor:from="this.selectColor"
        color:raw="red"
      ></color-picker>
    </div>

    <div>
      <color-picker
        selectColor:from="this.selectColor"
        color:raw="yellow"
      ></color-picker>

      <color-picker
        selectColor:from="this.selectColor"
        color:raw="blue"
      ></color-picker>
    </div>

    <div class="pattern">
      {{# for(color of this.pattern) }}
        <span style="background-color: {{ color }}"></span>
      {{/ for }}
    </div>
  `;

  static props = {
    // DERIVED PROPERTIES
    pattern: {
      value({ listenTo, resolve }) {
        const list = new ObservableArray();

        listenTo("color-change", (ev, color) => {
          list.push(color);
        });

        resolve(list);
      }
    }
  };

  selectColor(color) {
    this.dispatch("color-change", [ color ]);
  }

  connected() {
    let playingSound = Promise.resolve(true);

    const play = (audioElement) => {
      playingSound = playingSound.then(() => {
        return new Promise(resolve => {
          audioElement.play().then(() => {
            audioElement.addEventListener("ended", () => {
              resolve(true);
            });
          });
        });
      });
    };

    const audioElements = {
      green: this.querySelector("audio.green"),
      red: this.querySelector("audio.red"),
      yellow: this.querySelector("audio.yellow"),
      blue: this.querySelector("audio.blue")
    };

    this.listenTo("color-change", (ev, color) => {
      play( audioElements[color] );
    });
  }
};
customElements.define("memory-game", Simon);
</script>
<style>
color-picker button {
  width: 50px;
  height: 50px;
  margin: 0;
  padding: 0;
  border: 0;
}

.pattern span {
  display: inline-block;
  width: 23px;
  height: 23px;
}
</style>

Clean up event listeners

In most cases, CanJS will automatically clean up event listeners, but there are a few situations where event listeners will need to be cleaned up manually. These are discussed in the following sections.

Clean up listeners set up in the connected lifecycle hook

Any listeners set up using listenTo in the connected lifecycle hook will be cleaned up automatically when the component is torn down:

static props = {
  // SIDE EFFECTS
  connected() {
    // this will be cleaned up automatically
    listenTo("name", () => {
      // ...
    });
  }
}

This clean up is done by stopListening, which is the default teardown function. A function can be returned from the connect to provide a custom teardown function. When doing this, it is necessary to call stopListening manually to clean up event listeners:

static props = {
  // SIDE EFFECTS
  connected() {
    listenTo("aProperty", () => {
      // ...
    });

    // custom teardown function
    return () => {
      // calling `stopListening` manually to clean up
      // event listeners set up with `listenTo`
      this.stopListening();
    };
  }
}

Event listeners set up with anything other than listenTo, as well as timers and anything else that needs to be cleaned up, can also be cleaned up in this teardown function:

static props = {
  // SIDE EFFECTS
  connected(el) {
    listenTo("aProperty", () => {
      // ...
    });

    const timeoutId = setInterval(() => {
      // ...
    });

    const jqueryEventHandler = () => {
      // ...
    };

    $(el).on("some-jquery-event", jqueryEventHandler);

    const vanillaEventHandler = () => {
      // ...
    };

    el.addEventListener("some-vanilla-event", vanillaEventHandler);

    return () => {
      // clean up `listenTo` event listeners
      this.stopListening();

      // clean up other event listeners
      $(el).off("some-jquery-event", jqueryEventHandler);
      el.removeEventListener("some-vanilla-event", vanillaEventHandler);

      // clean up timers
      clearTimeout( timeoutId );
    };
  }
}

Clean up listeners in a value definition

The same technique can be used to clean up listeners set up in a value definition. A function returned from the value function will be called when that property is no longer bound.

Event listeners set up using listenTo in a value definition will be cleaned up automatically. Other listeners should be cleaned up manually.

Here is an example:

  // DERIVED PROPERTIES
  namedCounter: {
    value({ listenTo, resolve }) {
      let count = 0;
      let name = this.name;

      resolve(`${name}: ${count}`);

      let timeoutId = setInterval(() => {
        count++;
        resolve(`${name}: ${count}`);
      }, 1000);

      listenTo("name", (ev, n) => {
        name = n;
        resolve(`${name}: ${count}`);
      });

      return () => {
        clearTimeout(timeoutId);
      };
    }
  }

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 6.0.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news