can-stache-element
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>
Testing
Custom elements have lifecycle methods that are automatically called by the browser.
connectedCallback
is called when the element is added to the pagedisconnectedCallback
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:
- initialize - to set up the element's properties
- render - to create the innerHTML of the element
- connect - to connect the element to the DOM
The disconnectedCallback
will call:
- 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>