I am trying to understand the newish HTML custom elements.
My goal is, given some array of data, create n instances of the custom element. For example, given a list of 10 users, create 10 user html objects.
Ok - so I define a custom element in the html
HTML
<template-user>
<div class="user-name"></div>
</template-user>
Then I create my controller
JS
class UserTemplate extends HTMLElement {
constructor(){
super();
this.username = this.querySelectorAll('[class="user-name"]')[0];
}
setName(name){
this.username.innerHtml = name;
}
}
customElements.define('template-user', UserTemplate);
The page loads fine, but now I am confused on how to reuse that element. If I was doing normal old school stuff, I'd have a for-loop pumping out some HTML strings and setting the innerHTML of something. But now I'd rather do something like
for(let i = 0; i < users.length; i++){
let userTemplate = new UserTemplate();
userTemplate.setName(user.name);
// append to user list, etc..
}
When I try to do this, it seems to almost work. But it cannot find username, ie this.querySelectorAll will return null. That's only when I try to construct a new instance of this element. How then, am I supposed to create new custom element DOM objects?
Make sure you understand the requirements and limitations of constructors for Web Components:
https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
4.13.2 Requirements for custom element constructors
When authoring custom element constructors, authors are bound by the following conformance requirements:
A parameter-less call to super() must be the first statement in the constructor body, to establish the correct prototype chain and this value before any further code is run.
A return statement must not appear anywhere inside the constructor body, unless it is a simple early-return (return or return this).
The constructor must not use the document.write() or document.open() methods.
The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.
The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.
In general, work should be deferred to connectedCallback as much as possible—especially work involving fetching resources or rendering. However, note that connectedCallback can be called more than once, so any initialization work that is truly one-time will need a guard to prevent it from running twice.
In general, the constructor should be used to set up initial state and default values, and to set up event listeners and possibly a shadow root.
Several of these requirements are checked during element creation, either directly or indirectly, and failing to follow them will result in a custom element that cannot be instantiated by the parser or DOM APIs. This is true even if the work is done inside a constructor-initiated microtask, as a microtask checkpoint can occur immediately after construction.
You could make changes similar to this:
class TemplateUser extends HTMLElement {
static get observedAttributes() {
return ['user-name'];
}
constructor(){
super();
this.attachShadow({mode:'open'});
this.shadowRoot.innerHTML = '<div></div>';
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
this.shadowRoot.firstChild.innerHTML = newVal;
}
}
get userName() {
return this.getAttribute('user-name');
}
set userName(name) {
this.setAttribute('user-name', name);
}
}
customElements.define('template-user', TemplateUser);
setTimeout( function () {
var el = document.querySelector('[user-name="Mummy"]');
el.userName = "Creature from the Black Lagoon";
}, 2000);
<template-user user-name="Frank N Stein"></template-user>
<template-user user-name="Dracula"></template-user>
<template-user user-name="Mummy"></template-user>
This uses shadowDOM to store a <div>, then you set the value through the user-name attribute or through the userName property.
But it cannot find username, ie this.querySelectorAll will return null.
When you make a new instance, the new element has no children so querySelectorAll will return an empty NodeList. If you query the DOM and select the template-user which has been defined in your markup then the username property will refer to your div element.
If you want a dynamically generated template-user element to have a <div class="user-name"></div> child by default, you should create and append an element in your constructor.
Also for selecting the first matching element you can use .querySelector(...) instead of .querySelectorAll(...)[0].
When you create a custom element via Javascript with new, you can set some variables that will be passed as parameters in the constrcutor() method:
for (let user of users) {
document.body.appendChild( new UserTemplate(user.name) )
}
You can get it and save it in an object variable, or use in as a variable in a template literal string in a Shadow DOM.
class UserTemplate extends HTMLElement {
constructor(username){
super()
//this.username = username
this.attachShadow({ mode:'open' })
.innerHTML = `<div class="user-name"> ${username} </div>`
}
}
customElements.define('template-user', UserTemplate);
you can create your components and assign properties right in JS
let templateUser = document.createElement('template-user');
templateUser.userName= 'Your name here';
document.body.appendChild(templateUser);
or something similar to that based on your needs.
Google has some docs here, about 2/3 the way down they describe how to "create an instance in JavaScript". Which can be pretty powerful, especially in a for-loop like your example
Related
I'm trying to learn vanilla webcomponents and I am stuck trying to do something simple and no combination of keywords I can think of return anything helpful in Google.
So in my render method, I see all sorts of examples that manually construct a tag and assign all the attribute, like so:
render()
{
this.innerHTML = `
${
this.data
.map(reason =>
`<${ReasonItem.elementName}
name="${reason.Name}"
description="${reason.Description}">
</${ReasonItem.elementName}>`)
.join('')
}
`;
}
This works, but it's extremely tedious for child controls that have LOTS of attributes.
I would like to do something like:
render()
{
this.innerHTML = `
${
this.data
.map(reason =>
{
let child = new ReasonItem();
child.reason = reason;
child.render();
return child.outerHTML;
})
.join('')
}
`;
This almost works, but apparently the constructor and other methods on an HTMLElement can be called out of order so I'm getting 'unidentified' in all my elements since the constructor calls render() and the setting of the property calls render(), but the constructors render is being called after the property is set, so I'm thinking I'm not doing this right.
Thank you for your time and attention.
since the constructor calls render()
That should never be done in the constructor. Leave that call to connectedCallback.
but the constructor's render is being called after the property is set
That's not possible unless you have an designed an async constructor.
Also, if you're not using Stencil or lit which do property binding and updating for you, you shouldn't try to mimic a render method to react to updates.
Instead, create your elements programmatically and have the component store references to those elements, so you can make targeted, partial updates later. Use getters and setters for the purpose.
Edit: Your solution code can be simplified:
render()
{
while(this.lastChild) this.lastChild.remove();
this.append(...this.data.map(reason => Object.assign(new ReasonItem, { reason })))
}
Also you probably need to empty your host element before rendering if you insist on a render() method.
#connexo So after lots of guess and testing, this appears to do what I need:
render()
{
this.data.forEach((reason) =>
{
const el = document.createElement(ReasonItem.elementName);
el.reason = reason;
this.appendChild(el);
});
}
I like the look of this better, not sure if it's equivalent or advisable, perhaps javascript pros can tell me what's 'generally preferred'?
render()
{
this.data.forEach((reason) =>
{
const el = new ReasonItem();
el.reason = reason;
this.appendChild(el);
});
}
Internally in my 'ReasonItem', the 'reason' property is observed and when changed, it loads (from the complex reason json object) all the dozens of attributes into their appropriate spots on the ReasonItem component.
The code is as follows
class ComposerForm extends BaseForm {
constructor(formsObject, options) {
super({
...options,
setup: {},
});
this.formsObject = { ...formsObject };
}
..
}
Now i have a new form
class PreferencesForm extends ComposerForm {
constructor(company, options = {}) {
super(
{
upids: new UpidsForm(company).initialize(),
featureSettings: new FeatureSettingsForm(company)
},
options
);
}
}
When initialising the FeatureSettingsForm, i need to pass the Preference form along with the company object
Something like
{
featureSettings: new FeatureSettingsForm(company, {prefForm: this})
},
so that i can access the preference form inside featureSettings form.
But this cannot be done since this cannot be accessed inside the super method.
Any idea on how to achieve this?
If I understand you right,
You need to pass a FeatureSettingsForm instance in the object you're passing to super (ComposerForm) in the PreferencesForm constructor, and
You need this in order to create the FeatureSettingsForm instance
So you have a circular situation there, to do X you need Y but to do Y you need X.
If that summary is correct, you'll have to¹ change the ComposerForm constructor so that it allows calling it without the FeatureSettingsForm instance, and add a way to provide the FeatureSettingsForm instance later, (by assigning to a property or calling a method) once the constructor has finished, so you can access this.
¹ "...you'll have to..." Okay, technically there's a way around it where you could get this before calling the ComposerForm constructor, by falling back to ES5-level ways of creating "classes" rather than using class syntax. But it general, it's not best practice (FeatureSettingsForm may expect the instance to be fully ready) and there are downsides to have semi-initialized instances (that's why class syntax disallows this), so if you can do the refactoring above instead, that would be better. (If you want to do the ES5 thing anyway, my answer here shows an example of class compared to the near-equivalent ES5 syntax.)
So I'm trying to build a custom component using vanilla javascript, which will do certain things depending on the number of children it has, meaning it has to count said children
If I have the following markup (where the custom component is called "my-component")
<my-component>
<div></div>
<!-- ...arbitrary number of child elements -->
</my-component>
And the following javascript code in the <head></head> to ensure it's loaded before the <body></body> is parsed
class MyComponent extends HTMLElement {
constructor(){
super()
this.children.length
//do stuff depending on the number of children
}
//or
connectedCallback () {
this.children.length
//do stuff depending on the numbre of children
}
}
customElements.define("my-component",MyComponent)
this.children.length will return 0 in both cases, despite the elements showing on the screen afterwards, and being able to inspect the custom element on the console and get the expected number of children with Element.children.length. I suppose that this means the children elements are not yet available at the time the constructor nor the connectedCallback are run.
Is there any way to specify in my element's class definition a function that will trigger when the children elements become available, so that I can do stuff with them? I was hoping for a "childElementsReady" callback or something similar, but I guess that it doesn't exist. I don't know if there's a really obvious way to deal with this that I'm just missing, because this seems like something that I should be able to do relatively easily
A MutationObserver is the best way to handle this. You can set one up in connectedCallback to observe changes to the Light DOM - in this case it's enough to observe childList only:
class MyElement extends HTMLElement {
constructor() {
super();
this.onMutation = this.onMutation.bind(this);
}
connectedCallback() {
// Set up observer
this.observer = new MutationObserver(this.onMutation);
// Watch the Light DOM for child node changes
this.observer.observe(this, {
childList: true
});
}
disconnectedCallback() {
// remove observer if element is no longer connected to DOM
this.observer.disconnect();
}
onMutation(mutations) {
const added = [];
// A `mutation` is passed for each new node
for (const mutation of mutations) {
// Could test for `mutation.type` here, but since we only have
// set up one observer type it will always be `childList`
added.push(...mutation.addedNodes);
}
console.log({
// filter out non element nodes (TextNodes etc.)
added: added.filter(el => el.nodeType === Node.ELEMENT_NODE),
});
}
}
customElements.define('my-element', MyElement);
Here onMutation will be called every time nodes are added to the Light DOM so you can handle any set up here.
Note that, depending on the nodes in the Light DOM, onMutation can be called more than once when the element is connected to the DOM so it's not possible to say that all the children are 'ready' at any point - instead you must handle each mutation as it comes in.
I write this reply to my own question because I found out that this is an useful way to watch for added children when you have a shadow dom, so hopefully it can help anyone in that situation, but lamplightdev's answer is the most complete one since it works both when you use a shadow dom or not, so look up his answer as well
If your custom element makes use of the shadow dom, you can do this:
class MyComponent extends HTMLElement {
childAddedCustomCallback () {
let watchedSlot = this
/*"this" here is not the custom element, but a slot that the custom
element will have embedded in its shadow root, to which this function
will be attached as an event listener in the constructor*/
let children = watchedSlot.assignedElements()
let numberOfChildren = children.length
//do stuff depending on the number of children
}
constructor(){
super()
let shadowRoot = this.attachShadow({mode:"open"})
shadowRoot.innerHTML = "<slot></slot>"
let slotToWatch = shadowRoot.querySelector("slot")
slotToWatch.addEventListener("slotchange",this.childAddedCustomCallback)
}
}
customElements.define("my-component",MyComponent)
With this, every time you add a child to the custom element, it will reflect to the unnamed slot, and the slot's event listener will trigger the callback when that happens, giving you a reliable and clear way to access the children of the element as soon as they're available
This has the downside of excecuting n times if you add n children to the custom element, e.g. if you have the following markup:
<my-component>
<div></div>
<div></div>
<div></div>
<div></div>
</my-component>
Instead of excecuting once when the last child element is added, it will excecute 4 times (one for every child), so beware of that
I have many instantiated objects which all require their own handling of a specific event.
I have a class foo:
export default class Foo(){
constructor(eventManager){//reference to an event manager class
eventManager.eventPool.push(this.eventHandler)
this.someProperty = 'hello world'
}
eventHandler(e){
// logic to handle passed in event args
console.log(this.someProperty) //any property I access is undefined, no matter what I try
}
}
I have a static event handling class
export default class EventManager(){
constructor(){
this.eventPool = []
window.addEventListener('mousemove', this.onMouseMove.bind(this), false)
}
onMouseMove(e){
if(this.eventPool.length > 0){
for(let i=0;i<this.eventPool.length; ++i){
this.eventPool[i](e)
}
}
}
}
However when I call the eventHandler of a class and access its properties, they are undefined, I tried to bind the eventHandler to the class but that didn't work either. I'm not sure how the references are being handled since java-scripts not statically typed (natively)
this is being used in the context of threejs to abstract event handling away to be able to handle user input in various different ways on mesh's/other intractable items in the scene. I am aware of three.js's EventDispatcher but it doesn't give me enough control of the event hierarchy, I plan to make complicated event chains that I would like to be all neatly handled in a class that would not require editing source code.
How do I allow own objects eventHandlers to be called from a class managing all the objects function references? on a certain event?
I think the main problem is
for(let evenHandler in this.eventPool)
this.eventPool is an array, for let eventHandler in will only get the key or index of the array, rather than the element value. Try for let eventHandler of or let eventHandlerValue = this.eventPool[eventHandler].
I stumbled on the answer whilst reading some of three's source code and found a similar pattern...
While the solution was to bind the function to the instance as I tried before, it had to actually be done WHEN YOU PASS IT INTO THE ARRAY, not before, or after. With the example provided it would look like this....
export default class Foo(){
constructor(eventManager){//reference to an event manager class
eventManager.eventPool.push(this.eventHandler.bind(this))//bind the method while we pass it in
this.someProperty = 'hello world'
}
eventHandler(e){
console.log(this.someProperty) //now the function refrence will correctly access the instances properties
}
When using HyperHTMLElement it's possible to access the contents of the component by simply using this.children or this.querySelector(), since it's an element.
But how would I achieve similar behavior when using hyper.Component?
The hypothetical example I have in mind is from React docs: https://facebook.github.io/react/docs/refs-and-the-dom.html - I'd like to focus a specific node inside my DOM.
I have a codepen sandbox where I'm trying to solve this: https://codepen.io/asapach/pen/oGvdBd?editors=0010
The idea is that render() returns the same Node every time, so I could save it before returning and access it later as this.node:
render() {
this.node = this.html`
<div>
<input type="text" />
<input type="button" value="Focus the text input" onclick=${this} />
</div>
`;
return this.node;
}
But that doesn't look clean to me. Is there a better way to do this?
The handleEvent pattern is there to help you. The idea behind that pattern is that you never need to retain DOM references when the behavior is event-driven, 'cause you can always retrieve nodes via event.currentTarget, always pointing at the element that had the listener attached, or event.target, suitable for clicks happened in other places too within a generic click handler attached to the wrap element, in your demo case the div one.
If you'd like to use these information, you can enrich your components using an attribute to recognize them, like a data-is="custom-text-input" on the root element could be, and reach it to do any other thing you need.
onclick(e) {
var node = e.target.closest('[data-is=custom-text-input]');
node.querySelector('[type=text]').focus();
}
You can see a working example in a fork of your code pen:
https://codepen.io/WebReflection/pen/RLNyjy?editors=0010
As alternative, you could render your component and address its content once as shown in this other fork:
https://codepen.io/WebReflection/pen/LzEmgO?editors=0010
constructor() {
super().node = this.render();
}
at the end of the day, if you are not using custom elements but just basic, good'ol DOM nodes, you can initialize / render them whenever you want, you don't need to wait for any upgrade mechanism.
What is both nice and hopefully secure here, is that there's no way, unless you explicitly expose it, to address/change/mutate the instance related to the DOM element.
I hope these possibilities answered your question.
This is something I've worked on in the past via https://github.com/joshgillies/hypercomponent
The implementation is actually quite trivial.
class ElementalComponent extends hyper.Component {
constructor () {
super()
const _html = super.html
this.html = (...args) => {
this.node = _html.apply(this, args)
return this.node
}
}
}
class HelloWorld extends ElementalComponent {
render () {
return this.html`<div>Hello World!</div>`
}
}
This works really well and is inline with your question. However, it's worth noting hyperHTML can render not only a single node but also multiple nodes. As an example:
hyper`<div>Hello World!</div>` // returns a single DOM Node
hyper`<div>Hello</div> <div>World!</div>` // returns multiple DOM Nodes as an Array.
So this.node in the above ElementalComponent can be either a DOM Node, or Array based on what the renderer is doing.