I began to use Web Components in Javascript. I have a class called VariantSelects and I want each instance to have the same value. So whenever something changes in instance A, instance B should receive the same value. How do I do that?
I minimized my code for presentation purpose. The actual amount of functions is more complex.
<!-- instance A -->
<variant-selects></variant-selects>
<!-- instance B -->
<variant-selects></variant-selects>
class VariantSelects extends HTMLElement {
constructor() {
super();
this.currentVariant = null;
this.addEventListener('change', this.onVariantChange);
this.init();
}
init() {
// do stuff
}
onVariantChange() {
// should be updated in all instances
this.currentVariant = 123;
}
}
customElements.define('variant-selects', VariantSelects);
Thanks in advance!
I think I figured it out, although this might not be the best solution...
const allVariantRadios = document.querySelectorAll('variant-radios');
Array.from(allVariantRadios).forEach(radio => {
radio.addEventListener('change', updateAllInstances)
});
function updateAllInstances(e) {
allVariantRadios.forEach(instance => {
instance.querySelector(`input[value="${e.target.value}"]`).checked = true;
instance.onVariantChange();
});
}
Related
I have a Custom Element that should have many HTML children. I had this problem when initializing it in class' constructor (The result must not have children). I understand why and know how to fix it. But exactly how I should design my class around it now? Please consider this code:
class MyElement extends HTMLElement {
constructor() {
super();
}
// Due to the problem, these codes that should be in constructor are moved here
connectedCallback() {
// Should have check for first time connection as well but ommited here for brevity
this.innerHTML = `<a></a><div></div>`;
this.a = this.querySelector("a");
this.div = this.querySelector("div");
}
set myText(v) {
this.a.textContent = v;
}
set url(v) {
this.a.href = v;
}
}
customElements.define("my-el", MyElement);
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el); // connectedCallback is not called yet since it's not technically connected to the document.
el.myText = "abc"; // Now this wouldn't work because connectedCallback isn't called
el.url = "https://www.example.com/";
Since MyElement would be used in a list, it's set up beforehand and inserted into a DocumentFragment. How do you handle this?
Currently I am keeping a list of pre-connected properties and set them when it's actually connected but I can't imagine this to be a good solution. I also thought of another solution: have an init method (well I just realized nothing prevents you from invoking connectedCallback yourself) that must be manually called before doing anything but I myself haven't seen any component that needs to do that and it's similar to the upgrade weakness mentioned in the above article:
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.
Custom elements are tricky to work with.
The shadowDOM
if the shadowDOM features and restrictions suits your needs, you should go for it, it's straightforward :
customElements.define('my-test', class extends HTMLElement{
constructor(){
super();
this.shadow = this.attachShadow({mode: 'open'});
const div = document.createElement('div');
div.innerText = "Youhou";
this.shadow.appendChild(div);
}
});
const myTest = document.createElement('my-test');
console.log(myTest.shadow.querySelector('div')); //Outputs your div.
More about it there
Without shadowDOM
Sometimes, the shadowDOM is too restrictive. It provides a really great isolation, but if your components are designed to be used in an application and not be distributed to everyone to be used in any project, it can really be a nightmare to manage.
Keep in mind that the solution I provide below is just an idea of how to solve this problem, you may want to manage much more than that, especialy if you work with attributeChangedCallback, if you need to support component reloading or many other use cases not covered by this answer.
If, like me, you don't want the ShadowDOM features, and there is many reasons not to want it (cascading CSS, using a library like fontawesome without having to redeclare the link in every component, global i18n mechanism, being able to use a custom component as any other DOM tag, and so on), there is some clue :
Create a base class that will handle it in the same way for all components, let's call it BaseWebComponent.
class BaseWebComponent extends HTMLElement{
//Will store the ready promise, since we want to always return
//the same
#ready = null;
constructor(){
super();
}
//Must be overwritten in child class to create the dom, read/write attributes, etc.
async init(){
throw new Error('Must be implemented !');
}
//Will call the init method and await for it to resolve before resolving itself.
//Always return the same promise, so several part of the code can
//call it safely
async ready(){
//We don't want to call init more that one time
//and we want every call to ready() to return the same promise.
if(this.#ready) return this.#ready
this.#ready = new Promise(resolve => resolve(this.init()));
return this.#ready;
}
connectedCallback(){
//Will init the component automatically when attached to the DOM
//Note that you can also call ready to init your component before
//if you need to, every subsequent call will just resolve immediately.
this.ready();
}
}
Then I create a new component :
class MyComponent extends BaseWebComponent{
async init(){
this.setAttribute('something', '54');
const div = document.createElement('div');
div.innerText = 'Initialized !';
this.appendChild(div);
}
}
customElements.define('my-component', MyComponent);
/* somewhere in a javascript file/tag */
customElements.whenDefined('my-component').then(async () => {
const component = document.createElement('my-component');
//Optional : if you need it to be ready before doing something, let's go
await component.ready();
console.log("attribute value : ", component.getAttribute('something'));
//otherwise, just append it
document.body.appendChild(component);
});
I do not know any approach, without shdowDOM, to init a component in a spec compliant way that do not imply to automaticaly call a method.
You should be able to call this.ready() in the constructor instead of connectedCallback, since it's async, document.createElement should create your component before your init function starts to populate it. But it can be error prone, and you must await that promise to resolve anyway to execute code that needs your component to be initialized.
You need (a) DOM to assign content to it
customElements.define("my-el", class extends HTMLElement {
constructor() {
super().attachShadow({mode:"open"}).innerHTML=`<a></a>`;
this.a = this.shadowRoot.querySelector("a");
}
set myText(v) {
this.a.textContent = v;
}
});
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el);
el.myText = "abc";
document.body.append(frag);
Without shadowDOM you could store the content and process it in the connectedCallback
customElements.define("my-el", class extends HTMLElement {
constructor() {
super().atext = "";
}
connectedCallback() {
console.log("connected");
this.innerHTML = `<a>${this.atext}</a>`;
this.onclick = () => this.myText = "XYZ";
}
set myText(v) {
if (this.isConnected) {
console.warn("writing",v);
this.querySelector("a").textContent = v;
} else {
console.warn("storing value!", v);
this.atext = v;
}
}
});
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el);
el.myText = "abc";
document.body.append(frag);
Since there are many great answers, I am moving my approach into a separate answer here. I tried to use "hanging DOM" like this:
class MyElement extends HTMLElement {
constructor() {
super();
const tmp = this.tmp = document.createElement("div"); // Note in a few cases, div wouldn't work
this.tmp.innerHTML = `<a></a><div></div>`;
this.a = tmp.querySelector("a");
this.div = tmp.querySelector("div");
}
connectedCallback() {
// Should have check for first time connection as well but ommited here for brevity
// Beside attaching tmp as direct descendant, we can also move all its children
this.append(this.tmp);
}
set myText(v) {
this.a.textContent = v;
}
set url(v) {
this.a.href = v;
}
}
customElements.define("my-el", MyElement);
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el); // connectedCallback is not called yet since it's not technically connected to the document.
el.myText = "abc"; // Now this wouldn't work because connectedCallback isn't called
el.url = "https://www.example.com/";
document.body.append(frag);
It "works" although it "upsets" my code a lot, for example, instead of this.querySelector which is more natural, it becomes tmp.querySelector. Same in methods, if you do a querySelector, you have to make sure tmp is pointing to the correct Element that the children are in. I have to admit this is probably the best solution so far.
I'm not exactly sure about what makes your component so problematic, so I'm just adding what I would do:
class MyElement extends HTMLElement {
#a = document.createElement('a');
#div = document.createElement('div');
constructor() {
super().attachShadow({mode:'open'}).append(this.#a, this.#div);
console.log(this.shadowRoot.innerHTML);
}
set myText(v) { this.#a.textContent = v; }
set url(v) { this.#a.href = v; }
}
customElements.define("my-el", MyElement);
const frag = new DocumentFragment();
const el = document.createElement("my-el");
el.myText = 'foo'; el.url= 'https://www.example.com/';
frag.append(el);
document.body.append(el);
I don't quite know how to phrase this so I'll just dive ahead and try to explain it as best as I can.
I don't quite know how to store class properties without using them, or something like that.
If we have a class of Message that contains a method called addMessage.
We want to use this method. inside of a class called Magic.
How exactly do we do this?
class Message {
constructor(messageContainer) {
this.message = messageContainer;
}
addMessage(msg) {
this.message.innerText = msg;
}
}
class Magic extends Message {
constructor(magic) {
super();
this.magic = magic;
}
something() {
this.addMessage('Some random message');
}
}
We do:
const message = new Message(document.getElementById('message-container'))
in the code but never use message anywhere.
Instead we use Magic like so:
const magic = new Magic(something)
and then:
element.addEventListener('click', () => magic.something())
This does not work, it does seem logical to why it doesn't, its because message never gets used, but how could we make use of the addMessage method inside of Magic though?
I've tried a few things but nothing seems to work except for doing document.getElementById inside of the super(), but that seems to me like it defeats the point of using classes if I can't call it somehow in the code and have it keep the reference to the message container...
Any ideas?
The Magic constructor needs to take a messageContainer parameter to pass along to the Message constructor.
class Magic extends Message {
constructor(messageContainer, magic) {
super(messageContainer);
this.magic = magic;
}
something() {
this.addMessage('Some random message');
}
}
const magic = new Magic(document.getElementById('message-container'), something)
If you have lots of parameters and don't want to have list them all when calling super(), use an object to hold them instead of using seperate parameters. Each class can use the object properties that pertain to it.
class Message {
constructor(options) {
this.message = options.messageContainer;
}
addMessage(msg) {
this.message.innerText = msg;
}
}
class Magic extends Message {
constructor(options) {
super(options);
this.magic = options.magic;
}
something() {
this.addMessage('Some random message');
}
}
magic = new Magic({
messageContainer: document.getElementById("message-container"),
magic: something
});
Instead of inheritance, your use case is composed(Behavioral design patterns). Combining 2 objects to work together. here is basic sample to solve this.
class MessageContainer {
constructor(messageContainer) {
this.message = messageContainer;
}
addMessage(msg) {
this.message.innerHTML = msg;
}
}
class Magic {
constructor(msgContainer) {
this.container = msgContainer;
}
something() {
this.container.addMessage("Some random message"+ new Date().getTime());
}
}
const container = new MessageContainer(
document.getElementById("message-container")
);
const magic = new Magic(container);
document.addEventListener("click", () => {
magic.something();
});
<div style="height: 100px; color: red;">
click here
<br/>
<span id="message-container"></span>
</div>
This questions is actually React JS related. Is it OK to define internal class variables inside one of the class methods and then use it in other methods? I mean to do something like this:
class Something extends React.Component {
state = {
value: 'doesnt matter'
};
something = () => {
//a lot is happening in here and at some point I define this.thing
this.thing = 1;
}
increase = () => {
if(this.thing) {
this.thing += 1;
}
}
decrease = () => {
if(this.thing && this.thing > 0) {
this.thing -= 1;
}
}
render() {
return (
<span>this.state.value</span>
);
}
}
thing is that I don't need to put that this.thing as a state value, because I only need it internally. Please be aware that this code is just an example, real code is a bit more complicated, but the main question, is it OK to define class internal variables(this.thing) like I do in my example? Or maybe I should do this differently? What would be the best practice?
It's not a problem to use the constructor to do such a thing but based on the react theory and UI rendering this kind of usage will not re-render or follow the react pattern of trigger and re-render, it will just server as a storage for a value that has nothing to do with the react life cycle.
class Something extends React.Component {
constructor(props) {
super(props);
// Your thing
this.thing = 0;
this.state = {
value: "doesnt matter."
};
}
something = () => {
//a lot is happening in here and at some point I define this.thing
this.thing = 1;
};
increase = () => {
if (this.thing) {
this.thing += 1;
}
};
decrease = () => {
if (this.thing && this.thing > 0) {
this.thing -= 1;
}
};
render() {
this.something();
console.log(this.thing); // will equal 1.
return <span>{this.state.value}</span>;
}
}
I don't need to put that this.thing as a state value, because I only need it internally.
A React component's state should also only be used internally.
What would be the best practice?
You can use instance variables (ivars) instead of state to increase performance because you may reduce the burden on the event queue. Aesthetically, ivars often require less code. But state updates are usually preferred because they will trigger a re-render; this guarantee makes your code easier to think about, as the render is never stale. In your case, the render function is independent of this.thing, so it's okay to store it in an ivar.
Generally, it's best to initialize ivars in the constructor because it runs first, so this.thing is guaranteed to be ready for consumption by other methods:
constructor(props) {
super(props);
this.thing = 0;
}
I am creating a PDF like this inside a react Component.
export class Test extends React.PureComponent {
savePDF() {
const source = document.getElementById('printContainer');
/* eslint new-cap: ["error", { "newIsCap": false }]*/
let pdf = new jspdf('p', 'pt', 'letter');
let margins = { top: 50,
left: 60,
width: 612
};
pdf.fromHTML(
source,
margins.left,
margins.top,
{
width: margins.width
},
() => {
pdf.save('worksheet.pdf');
}
);
}
and I am getting warning Expected 'this' to be used by class method 'savePDF' class-me
this is being called an click like this onClick={this.savePDF} see below
render() {
<Link
name="save-to-pdf"
onClick={this.savePDF}
button="secondary">
Save to PDF</Link>
<div id="printContainer" className="cf-app-segment--alt cf-hearings-worksheet">...
There are two different answers to this question, depending on how you want to handle it.
First, the reason you get this error is because of the ESLint rule https://eslint.org/docs/rules/class-methods-use-this. Specifically, this is because if something is a class method, e.g. if you are calling this.foo() to call a function, the whole reason to make it a method is because there are properties on this that you need to use.
While in many languages with class, most functions are methods, that is not the case in JS. If you have a class like
class Example {
constructor(){
this.data = 42;
}
someMethod() {
this.someHelper(this.data);
}
someHelper(value){
console.log(value);
}
}
the someHelper function would trigger the same error you are getting, because it never uses this, so you can just as easily do
class Example {
constructor(){
this.data = 42;
}
someMethod() {
someHelper(this.data);
}
}
function someHelper(value){
console.log(value);
}
In your case, you can do this. Your whole savePDF function could be moved outside of the class object.
That said, it is important to ask yourself why something like this isn't using this. In most cases, you'd expect any function that works with HTML to absolutely use this, because how else, in React, is it supposed to access the element's that React has created.
So the real answer to your question would be to drop the
const source = document.getElementById('printContainer');
line. If you need access to the HTML element being created by React, you should be using React's APIs to do so. That would be done with something like
class SavePDFButton extends React.Component {
constructor(props) {
super(props);
this.printContainer = null;
this.savePDF = this.savePDF.bind(this);
this.handlePrintContainerRef = this.handlePrintContainerRef.bind(this);
}
render() {
return (
<div>
<Link
name="save-to-pdf"
onClick={this.savePDF}
button="secondary"
>
Save to PDF
</Link>
<div
id="printContainer"
className="cf-app-segment--alt cf-hearings-worksheet"
ref={this.handlePrintContainerRef}
/>
</div>
);
}
handlePrintContainerRef(el) {
// When React renders the div, the "ref={this.handlePrintContainerRef}" will
// make it call this function, which will store a reference.
this.printContainer = el;
}
savePDF() {
// OLD: const source = document.getElementById('printContainer');
const source = this.printContainer;
// ...
}
}
I believe that's caused by the class-methods-use-this ESLint rule.
It's just letting you know that your function doesn't use this, so you can probably make it a static function.
turn it into static function
static savePDF() { ... }
Its happening because this function isnt using this meaning it dosnt need to be dynamic
I'm playing with angular2 alpha 40 with ng2-play starter from pawel.
Examples are in typescript.
I have a service MovieList like this:
export class Movie {
selected: boolean = false
constructor(public name:string, public year:number, public score:number) {}
}
export class MovieListService {
list: Array<Movie>
selectMovie = new EventEmitter();
constructor() {
this.list = [new Movie('Star Wars', 1977, 4.4)];
}
add(m:Movie) {
this.list.push(m);
}
remove(m:Movie) {
for(var i = this.list.length - 1; i >= 0; i--) {
if(this.list[i] === m) {
if(m.selected) this.selectMovie.next();
this.list.splice(i, 1);
}
}
}
select(m:Movie) {
this.list.map((m) => m.selected = false);
m.selected = true;
this.selectMovie.next(m);
}
}
I have a component showing the movies list and make possible to select one by clicking on it, which call select() in the service above.
And I have another component (on the same level, I don't want to use (selectmovie)="select($event)") which subscribe to the movie selection event like this:
#Component({
selector: 'movie-edit',
})
#View({
directives: [NgIf],
template: `
<div class="bloc">
<p *ng-if="currentMovie == null">No movie selected</p>
<p *ng-if="currentMovie != null">Movie edition in progress !</p>
</div>
`
})
export class MovieEditComponent {
currentMovie:Movie
constructor(public movieList: MovieListService) {
this.movieList.selectMovie.toRx().subscribe(this.movieChanged);
setTimeout(() => { this.movieChanged('foo'); }, 4000);
}
movieChanged(f:Movie = null) {
this.currentMovie = f;
console.log(this.currentMovie);
}
}
The event is subscribed using .toRx().subscribe() on the eventEmitter.
movieChanged() is called but nothing happen in the template..
I tried using a timeout() calling the same function and changes are refleted in the template.
The problem seems to be the fact that subscribe expects an Observer or three functions that work as an observer while you are passing a normal function. So in your code I just changed movieChanged to be an Observer instead of a callback function.
movieChanged: Observer = Observer.create(
(f) => { this.currentMovie = f; }, // onNext
(err) => {}, // onError
() => {} // onCompleted
);
See this plnkr for an example. It would have been nice to see a minimal working example of your requirement so my solution would be closer to what you are looking for. But if I understood correctly this should work for you. Instead of a select I just used a button to trigger the change.
Update
You can avoid creating the Òbserver just by passing a function to the subscriber method (clearly there's a difference between passing directly a function and using a class method, don't know really why is different)
this.movieList.selectMovie.toRx().subscribe((m: Movie = null) => {
this.currentMovie = m;
});
Note
EventEmitter is being refactored, so in future releases next will be renamed to emit.
Note 2
Angular2 moved to #reactivex/rxjs but in the plnkr I'm not able to use directly those libs (didn't find any cdn). But you can try in your own project using these libs.
I hope it helps.
The movieChanged function expects the movie object and not the String. Try changing below code
setTimeout(() => { this.movieChanged('foo'); }, 4000);
to
setTimeout(() => { this.movieChanged(new Movie('Troy', 2000 , 8)); }, 4000);