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
  • API Docs
    • Observables
      • can-bind
      • can-compute
      • can-debug
      • can-deep-observable
      • can-define
      • can-define/list/list
      • can-define/map/map
      • can-define-backup
      • can-define-stream
      • can-define-stream-kefir
      • can-event-queue
      • can-kefir
      • can-list
      • can-map
      • can-map-compat
      • can-map-define
      • can-observable-array
      • can-observable-object
      • can-observation
      • can-observation-recorder
      • can-observe
      • can-simple-map
      • can-simple-observable
      • can-stream
      • can-stream-kefir
      • can-value
    • Views
      • can-attribute-observable
      • can-component
      • can-observable-bindings
      • can-stache
      • can-stache-bindings
      • can-stache-converters
      • can-stache-element
        • static
          • view
          • props
        • lifecycle methods
          • connectedCallback
          • initialize
          • render
          • connect
          • disconnectedCallback
          • disconnect
          • bindings
        • lifecycle hooks
          • connected
          • disconnected
      • can-stache-route-helpers
      • can-view-autorender
      • can-view-callbacks
      • can-view-import
      • can-view-model
      • can-view-parser
      • can-view-scope
      • can-view-target
      • steal-stache
    • Data Modeling
      • can-connect
      • can-connect-feathers
      • can-connect-ndjson
      • can-connect-tag
      • can-define-realtime-rest-model
      • can-define-rest-model
      • can-fixture
      • can-fixture-socket
      • can-local-store
      • can-memory-store
      • can-ndjson-stream
      • can-query-logic
      • can-realtime-rest-model
      • can-rest-model
      • can-set-legacy
      • can-super-model
    • Routing
      • can-deparam
      • can-param
      • can-route
      • can-route-hash
      • can-route-mock
      • can-route-pushstate
    • JS Utilities
      • can-assign
      • can-define-lazy-value
      • can-diff
      • can-globals
      • can-join-uris
      • can-key
      • can-key-tree
      • can-make-map
      • can-parse-uri
      • can-queues
      • can-string
      • can-string-to-any
      • can-zone-storage
    • DOM Utilities
      • can-ajax
      • can-attribute-encoder
      • can-child-nodes
      • can-control
      • can-dom-data
      • can-dom-events
      • can-dom-mutate
      • can-event-dom-enter
      • can-event-dom-radiochange
      • can-fragment
    • Data Validation
      • can-define-validate-validatejs
      • can-type
      • can-validate
      • can-validate-interface
      • can-validate-legacy
      • can-validate-validatejs
    • Typed Data
      • can-cid
      • can-construct
      • can-construct-super
      • can-data-types
      • can-namespace
      • can-reflect
      • can-reflect-dependencies
      • can-reflect-promise
      • can-types
    • Polyfills
      • can-symbol
      • can-vdom
    • Core
    • Infrastructure
      • can-global
      • can-test-helpers
    • Ecosystem
    • Legacy
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

can-stache-element

  • npm package badge
  • Star
  • Edit on GitHub

Create a custom element with ObservableObject-like properties and stache views.

StacheElement

can-stache-element exports a StacheElement class used to define custom elements.

Extend StacheElement with a:

  • static view - A stache view.
  • static props - ObservableObject-like property definitions.
  • getters, setters, and methods.
  • lifecycle hooks - connected and disconnected.

The following defines a <count-er> element:

<count-er></count-er>
<script type="module">
import { StacheElement } from "can/everything";
class Counter extends StacheElement {
  static view = `
    Count: <span>{{ this.count }}</span>
    <button on:click="this.increment()">+1</button>
  `;
  static props = {
    count: 0
  };
  increment() {
    this.count++;
  }
}
customElements.define("count-er", Counter);
</script>

To create an element instance, either:

  • Write the element tag and bindings in a can-stache template like:
    <count-er count:from="5"/>
    
  • Write the element tag in an HTML page:
    <count-er></count-er>
    
  • Create an instance of the class programmatically like:
    const myCounter = new Counter();
    document.body.appendChild(myCounter);
    myCounter.count = 6;
    myCounter.innerHTML; //-> Count: <span>6</span>...
    

Basic Use

The following sections cover everything you need to create a custom element with StacheElement.

Defining a custom element with a StacheElement constructor

In order to create a basic custom element with StacheElement, create a class that extends StacheElement and call customElements.define with the tag for the element and the constructor:

<count-er></count-er>
<script type="module">
import { StacheElement } from "can/everything";
class Counter extends StacheElement {
}
customElements.define("count-er", Counter);
</script>

This custom element can be used by putting a <count-er></count-er> tag in an HTML page.

Defining an element's view

StacheElement uses can-stache to render live-bound HTML as the element's innerHTML.

To create a can-stache view for the element, add a static view property to the class:

<count-er></count-er>
<script type="module">
import { StacheElement } from "can/everything";
class Counter extends StacheElement {
  static view = `
    Count: <span>{{ this.count }}</span>
    <button on:click="this.increment()">+1</button>
  `;
}
customElements.define("count-er", Counter);
</script>

The element's HTML will automatically update when any of the element's properties used by the view change.

Defining an element's properties

To manage the logic and state of an element, ObservableObject-like property definitions can be added to explicitly configure how an element's properties are defined.

To add property definitions, add a static props object to the class:

<count-er></count-er>
<script type="module">
import { StacheElement } from "can/everything";
class Counter extends StacheElement {
  static view = `
    Count: <span>{{ this.count }}</span>
    <button on:click="this.increment()">+1</button>
  `;
  static props = {
    count: 6
  };
}
customElements.define("count-er", Counter);
</script>

Defining Methods, Getters, and Setters

Methods (as well as getters and setters) can be added to the class body as well:

<count-er></count-er>
<script type="module">
import { StacheElement } from "can/everything";
class Counter extends StacheElement {
  static view = `
    Count: <span>{{ this.count }}</span>
    <button on:click="this.increment()">+1</button>
  `;
  static props = {
    count: 6
  };
  increment() {
    this.count++;
  }
}
customElements.define("count-er", Counter);
</script>

Lifecycle hooks

If needed, connected and disconnected lifecycle hooks can be added to the class body. These will be called when the element is added and removed from the page, respectively.

<button id="add">Add Timer</button>
<button id="remove">Remove Timer</button>
<script type="module">
import { StacheElement } from "can/everything";

class Timer extends StacheElement {
  static view = `
    <p>{{ this.time }}</p>
  `;
  static props = {
    time: { type: Number, default: 0 },
    timerId: Number
  };
  connected() {
    this.timerId = setInterval(() => {
      this.time++;
    }, 1000);
    console.log("connected");
  }
  disconnected() {
    clearInterval(this.timerId);
    console.log("disconnected");
  }
}
customElements.define("time-er", Timer);

let timer;
document.body.querySelector("button#add").addEventListener("click", () => {
  timer = document.createElement("time-er");
  document.body.appendChild(timer);
});

document.body.querySelector("button#remove").addEventListener("click", () => {
  document.body.removeChild(timer);
});
</script>

Passing templates (customizing layout)

It's a very common need to customize the html of a custom element. For example, you might want a <hello-world> element to write out the "Hello World" message inside an <h1>, <h2> or any other DOM structure.

On a high level, this customization involves two steps:

  • Passing templates with <can-template>
  • Calling the templates with {{ this.template() }}

Passing templates with <can-template>

When rendering a StacheElement in a can-stache template, you can declaratively create and pass templates with the <can-template> element.

For example, one might want to customize one <hello-world> element to write out "Hello World" message in a <h1> or in an italic paragraph as follows:

<hello-world>
  <can-template name="messageTemplate">
    <h1>{{ message }}</h1>
  </can-template>
</hello-world>

<hello-world>
  <can-template name="messageTemplate">
    <p>I say "<i>{{ message }}</i>"!</p>
  </can-template>
</hello-world>

Here's what you need to know about <can-template>:

  • Every <can-template> MUST have a name attribute. This is the name of the property on the custom element that will be set to the template. In the previous example, both <hello-world>'s will be created with a messageTemplate property:

    document.querySelector("hello-world").messageTemplate //-> templateFunction()
    
  • You can have multiple <can-template>s within a custom element. For example, the following passes two templates to configure <hello-world>:

    <hello-world>
      <can-template name="greetingTemplate">
        <b>{{ greeting }}</b>
      </can-template>
      <can-template name="subjectTemplate">
        <b>{{ subject }}</b>
      </can-template>
    </hello-world>
    
  • <can-template>s have the same scope of the custom element, plus a LetScope that the custom element can optionally provide. This means that a {{ this.someData }} immediately outside the <hello-world> will reference the same value as {{ this.someData }} within a <can-template>:

    {{ this.someData }}
    <hello-world>
      <can-template name="messageTemplate">
        <h1>{{ message }}</h1>
        <p>Also: {{ this.someData }}</p>
      </can-template>
    </hello-world>
    

    The custom element can add additional data, like message, to these templates. We will see how to do that in the next section.

Calling the templates with {{ this.template() }}

Once templates are passed to a custom element, you can call those templates within the StacheElement's view. For example, <hello-world> might call a passed messageTemplate as follows:

<my-app></my-app>

<script type="module">
import { StacheElement } from "can/everything";

class HelloWorld extends StacheElement {
  static view = `
    <div>{{ this.messageTemplate() }}</div>
  `;
  static props = {
    messageTemplate: {type: Function, required: true},
    message: "Hello World"
  }
}
customElements.define("hello-world", HelloWorld);

class App extends StacheElement {
  static view = `
    <hello-world>
      <can-template name="messageTemplate">
        <h1>{{ message }}</h1>
      </can-template>
    </hello-world>
  `;
}
customElements.define("my-app", App);
</script>

While the above will render the passed messageTemplate, it will not provide it a {{ message }} variable that can be read. You can pass values into a template with a Hash Expression. The following passes the message:

<my-app></my-app>

<script type="module">
import { StacheElement } from "can/everything";

class HelloWorld extends StacheElement {
  static view = `
    <div>{{ this.messageTemplate(message = this.message) }}</div>
  `;
  static props = {
    messageTemplate: {type: Function, required: true},
    message: "Hello World"
  }
}
customElements.define("hello-world", HelloWorld);

class App extends StacheElement {
  static view = `
    <hello-world>
      <can-template name="messageTemplate">
        <h1>{{ message }}</h1>
      </can-template>
    </hello-world>
  `;
}
customElements.define("my-app", App);
</script>

Sometimes, instead of passing each variable, you might want to pass the entire custom element:

<my-app></my-app>

<script type="module">
import { StacheElement } from "can/everything";

class HelloWorld extends StacheElement {
  static view = `
    <div>{{ this.messageTemplate(helloWorld = this) }}</div>
  `;
  static props = {
    messageTemplate: {type: Function, required: true},
    message: "Hello World"
  }
}
customElements.define("hello-world", HelloWorld);

class App extends StacheElement {
  static view = `
    <hello-world>
      <can-template name="messageTemplate">
        <h1>{{ helloWorld.message }}</h1>
      </can-template>
    </hello-world>
  `;
}
customElements.define("my-app", App);
</script>

Finally, you might want to provide a default template if one is not provided. You can do this either in the view or as a default props value.

In the view:

<my-app></my-app>

<script type="module">
import { StacheElement } from "can/everything";

class HelloWorld extends StacheElement {
  static view = `
    {{# if( this.messageTemplate ) }}
      <div>{{ this.messageTemplate(helloWorld = this) }}</div>
    {{ else }}
      <h1>Default: {{this.message}}</h1>
    {{/ if }}
  `;
  static props = {
    messageTemplate: Function,
    message: "Hello World"
  }
}
customElements.define("hello-world", HelloWorld);

class App extends StacheElement {
  static view = `
    <hello-world>
    </hello-world>
  `;
}
customElements.define("my-app", App);
</script>

As a default props value:

<my-app></my-app>

<script type="module">
import { StacheElement, stache } from "can/everything";

class HelloWorld extends StacheElement {
  static view = `
    <div>{{ this.messageTemplate(helloWorld = this) }}</div>
  `;
  static props = {
    messageTemplate: {
      type: Function,
      default: stache(`<h1>Default: {{ helloWorld.message }}</h1>`)
    },
    message: "Hello World"
  }
}
customElements.define("hello-world", HelloWorld);

class App extends StacheElement {
  static view = `
    <hello-world>
    </hello-world>
  `;
}
customElements.define("my-app", App);
</script>

If a property changes, the rendered passed template also update its HTML like following:

<my-app></my-app>

<script type="module">
import { StacheElement, stache } from "can/everything";

class HelloWorld extends StacheElement {
  static view = `
    <div>{{ this.messageTemplate(helloWorld = this) }}</div>
  `;
  static props = {
    messageTemplate: {
      type: Function,
      default: stache(`<h1>Default: {{ helloWorld.message }}</h1>`)
    },
    message: "Hello World"
  }
  change() {
    this.message = "Hello CanJS"
  }
}
customElements.define("hello-world", HelloWorld);

class App extends StacheElement {
  static view = `
    <hello-world>
      <can-template name="messageTemplate">
        <h1>{{ helloWorld.message }}</h1>
        <button on:click="helloWorld.change()">Toggle</button>
      </can-template>
    </hello-world>
  `;
}
customElements.define("my-app", App);
</script>

Testing

Custom elements have lifecycle methods that are automatically called by the browser.

  • connectedCallback is called when the element is added to the page
  • disconnectedCallback is called when the element is removed from the page

StacheElement uses the custom element lifecycle methods to initiate its own lifecycle.

The connectedCallback will call:

  1. initialize - to set up the element's properties
  2. render - to create the innerHTML of the element
  3. connect - to connect the element to the DOM

The disconnectedCallback will call:

  1. disconnect - to clean up event handlers and call teardown functions

StacheElement's lifecycle methods can be used to test each part of the lifecycle. The following sections explain how to do this.

Testing an element's properties and methods

To test an element's properties and methods, call the initialize method with any initial property values:

import { StacheElement } from "can/everything";
class Counter extends StacheElement {
  static view = `
    Count: <span>{{ this.count }}</span>
    <button on:click="this.increment()">+1</button>
  `;
  static props = {
    count: 6
  };
  increment() {
    this.count++;
  }
}
customElements.define("count-er", Counter);
const counter = new Counter()
  .initialize({ count: 20 });

counter.count === 20; // -> true

counter.increment();
counter.count === 21; // -> true

Testing an element's view

To test an element's view, call the render method with any initial property values:

import { StacheElement } from "can/everything";
class Counter extends StacheElement {
  static view = `
    Count: <span>{{ this.count }}</span>
    <button on:click="this.increment()">+1</button>
  `;
  static props = {
    count: 6
  };
  increment() {
    this.count++;
  }
}
customElements.define("count-er", Counter);
const counter = new Counter()
  .render({ count: 20 });

counter.firstElementChild.innerHTML === "20"; // -> true

counter.increment();
counter.firstElementChild.innerHTML === "21"; // -> true

Testing an element's lifecycle hooks

To test the functionality of the connected or disconnected hooks, you can call the connect or disconnect method.

import { StacheElement } from "can/everything";

class Timer extends StacheElement {
  static view = `
    <p>{{ this.time }}</p>
  `;
  static props = {
    time: { type: Number, default: 0 },
    timerId: Number
  };
  connected() {
    this.timerId = setInterval(() => {
      this.time++;
    }, 1000);
  }
  disconnected() {
    clearInterval(this.timerId);
  }
}
customElements.define("time-er", Timer);

const timer = new Timer()
  .connect();

timer.firstElementChild; // -> <p>0</p>

// ...some time passes
timer.firstElementChild; // -> <p>42</p>

timer.disconnect();

// ...some moretime passes
timer.firstElementChild; // -> <p>42</p>

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