Web Components: Is there an equivalent to attributeChangedCallback for properties? - javascript

You're not supposed to put rich data (objects, arrays, functions) in HTML element attributes. Instead, it's suggested to only put rich data in properties (according to the Google custom elements best practices article). I need to run actions when these properties are updated. We have observedAttributes and attributeChangedCallback, but there's nothing similar for properties.
Let's say I have a user prop with things like name, DoB, and address on it. I thought I might be able to trick observedAttributes by putting a bunk setter a la
set user(val) {
return;
}
Didn't work. return this.user = val gives an infinite loop.
My only idea at this point is to have a property called _user that simply gets set to [Object object] on every change, which triggers the change I actually want. Don't really like that though.
UPDATE: This is what I'm currently doing
In user-info.js:
class UserInfo extends HTMLElement {
connectedCallback() {
subscribers.push({ element: this, props: ['user'] });
this._user = state.user;
this.render();
}
static get observedAttributes() {
return ['user'];
}
attributeChangedCallback(name, oldValue, newValue) {
this.render();
}
get user() {
return this._user;
}
set user(val) {
if (JSON.stringify(val) !== JSON.stringify(this._user)) {
this._user = val;
return this.setAttribute('user', val);
}
}
render() {
this.innerHTML = `<span>${this._user.name}</span> was born on <span>${this._user.dob}</span>`;
}
}
In main.js:
document.querySelector('.actions--user').addEventListener('input', e => {
state.user = {...state.user, [e.target.dataset.action]: e.target.value};
})

You can use a Proxy to detect updated properties of an object.
customElements.define( 'user-info', class extends HTMLElement {
connectedCallback() {
this._user = {
name: 'Bruno',
dob: '1/1/2000'
}
this.render();
this._proxy = new Proxy( this._user, {
set: ( obj, prop, val ) => {
if ( prop === 'name' )
if ( this._user.name !== val ) {
console.log( 'username updated to ' + val )
this._user.name = val
this.render()
}
}
} )
}
get user() {
return this._proxy
}
set user(val) {
if (JSON.stringify(val) !== JSON.stringify(this._user)) {
this._user = val
this.render()
}
}
render() {
this.innerHTML = `<span>${this._user.name}</span> was born on <span>${this._user.dob}</span>`
}
} )
<user-info id=ui></user-info><br>
<label>Name: <input oninput="ui.user.name=this.value"></label>
Alternately you could define a User object / class with setters that would interact with the custom element.
class User {
constructor( elem ) {
this._elem = elem
this._name = 'Bruno'
this._dob = '1/1/2000'
}
set name( val ) {
if ( val !== this._name ) {
this._name = val
this._elem.render()
}
return false
}
get name() {
return this._name
}
get dob() {
return this._dob
}
update( obj ) {
this._name = obj.name
this._dob = obj.dob
}
}
class UserInfo extends HTMLElement {
connectedCallback() {
this._user = new User( this )
this.render()
}
get user() {
return this._user
}
set user(val) {
this._user.update( val )
this.render()
}
render() {
this.innerHTML = `<span>${this._user.name}</span> was born on <span>${this._user.dob}</span>`
}
}
customElements.define( 'user-info', UserInfo )
<user-info id=ui></user-info><br>
<label>Name: <input oninput="ui.user.name=this.value"></label>

Related

createElement function of vue js doesn't return compeletly component

There is a situation which is I need to write a functional component for a specific cause. That is gets all of its children and adds some props to them. But the point is, I need to add those props to just a specific custom components which are of type ChildComponent. I went this way:
MyHigherOrderComponent.vue
render: function(createElement, context){
const preparedChildrenLevel1 = context.children.map(child => {
if(child.componentOptions.tag !== "ChildComponent"){
return child;
}
return createElement(
ChildComponent,
{
props: {
...child.componentOptions.propsData,
level1: "level1"
}
},
child.componentOptions.children
)
});
},
This actually works fine. Then I want to use preparedChildrenLevel1 to map throw it and add another prop to the children which are of type ChildComponent. But this time I get undefined from child.componentOptions.tag.
MyHigherOrderComponent.vue
render: function(createElement, context){
//First level of adding props to children
const preparedChildrenLevel1 = context.children.map(child => {
//In here child.componentOptions.tag is equal to 'ChildComponent'
if(child.componentOptions.tag !== "ChildComponent"){
return child;
}
return createElement(
ChildComponent,
{
props: {
...child.componentOptions.propsData,
level1: "level1"
}
},
child.componentOptions.children
)
});
//Socond level of adding props to children
const preparedChildrenLevel2 = preparedChildrenLevel1.map(child => {
//In here child.componentOptions.tag is equal to 'undefined'
if(child.componentOptions.tag !== "ChildComponent"){
return child;
}
return createElement(
ChildComponent,
{
props: {
...child.componentOptions.propsData,
level2: "level2"
}
},
child.componentOptions.children
)
});
},
I need to get this specific type of components in many levels.
Note: Here is my complete implementation of component and how I use it
MyHigherOrderComponent.vue
<script>
export default {
name: "MyHigherOrderComponent",
functional: true,
render: function(createElement, context){
//First level of adding props to children
const preparedChildrenLevel1 = context.children.map(child => {
//In here child.componentOptions.tag is equal to 'ChildComponent'
if(child.componentOptions.tag !== "ChildComponent"){
return child;
}
return createElement(
ChildComponent,
{
props: {
...child.componentOptions.propsData,
level1: "level1"
}
},
child.componentOptions.children
)
});
//Socond level of adding props to children
const preparedChildrenLevel2 = preparedChildrenLevel1.map(child => {
//In here child.componentOptions.tag is equal to 'undefined'
if(child.componentOptions.tag !== "ChildComponent"){
return child;
}
return createElement(
ChildComponent,
{
props: {
...child.componentOptions.propsData,
level2: "level2"
}
},
child.componentOptions.children
)
});
},
}
</script>
App.vue
<template>
<MyHigherOrderComponent>
<p>child1</p>
<p>child2</p>
<ChildComponent :level0="level0">child3</ChildComponent>
<p>child4</p>
<ChildComponent :level0="level0">child5</ChildComponent>
<ChildComponent :level0="level0">child6</ChildComponent>
</MyHigherOrderComponent>
</template>
When the element is not a ChildComponent there is no child.components in it so you should add another check in the conditions:
if(!child.componentOptions || child.componentOptions.tag !== "ChildComponent") {
return child;
}
This way it will ensure that only a ChildComponent is allowed into the rest of your code.
And by the way you're not returning anything in MyHigherOrderComponent's render function

Child Component is not updating after Parent Component passes it new props

When I get userInput from onChange() and try to pass that to the child Component It is not updating rather holding on to the initial value.
I'm trying to pass the string from input field to child component called Tensor-flow Toxic Model, however the state of TensorFlowToxidModel does not change after it is initially set. So I cant run the moddl.
class TensorFlowToxicModel extends React.Component<{}, ToxicityModelProp> {
constructor(props: userInput) {
super(props);
this.state = {
modelObjectArray: [],
userSentence: props.userSentence,
};
}
componentDidUpdate(){
console.log("This is from TensorFlowToxicModel Compononent")
console.log("This is the sentence ", this.state.userSentence )
}
renderThePost = () => {
let output = cleanMlOutput(this.state.userSentence)
return output
};
render() {
return (
<div>
<p>This is a Checker Does this even work</p>
</div>
);
}
}
class InputField extends React.Component<{}, userInput> {
constructor(prop: inputFromField) {
super(prop);
this.state = {
userSentence: "",
};
}
handleChange = (event: React.FormEvent<HTMLInputElement>): void => {
let userInputData: string = event.currentTarget.value;
//console.log(event.currentTarget.value);
this.setState({
userSentence: userInputData,
});
};
render() {
const userSentence = {
userSentence:this.state.userSentence
}
//Instead of updating TensorFlowToxicModel Each time from inside its own compnoent
//Call it here each time user types something
return (
<div>
<input id="inputField" onChange={this.handleChange} />
<h4>{this.state.userSentence}</h4>
<TensorFlowToxicModel {...userSentence}/>
</div>
);
}
}
the Types
type modelObject = { label: string; resultMatch: boolean; resultProbablity: number; };
type ToxicityModelProp = { userSentence: string; modelObjectArray : modelObject[] }
You're misplaced the prop types ToxicityModelProp. It should be on first. Read this docs for information about component props,state types
type ToxicityModelProp = { userSentence: string }
type ToxicityModelState = { modelObjectArray: [] }
class TensorFlowToxicModel extends React.Component<ToxicityModelProp, ToxicityModelState> {
constructor(props: userInput) {
super(props);
this.state = {
modelObjectArray: []
};
}
renderThePost = () => {
let output = cleanMlOutput(this.props.userSentence)
return output
};
render() {
return (
<div>
<p>Sentence is: { this.props.userSentence }</p>
</div>
);
}
}
I have made some changes on your code and update here. Check it out

HOC component - array return true or false

I'm having a mental block right now, I have a HOC component that's checking for a feature flag like below:
const withEnabledFeatures = (WrappedComponent: any) => {
class WithEnabledFeatures extends React.Component<any> {
enabledFeatures = (): Array<string> => {
if (window === undefined) return [];
return window.AH.featureFlags;
}
isFeatureEnabled = (feature: string): boolean => {
return this.enabledFeatures.includes(feature);
}
render() {
return (
<WrappedComponent
enabledFeatures={this.enabledFeatures()}
isFeatureEnabled={this.isFeatureEnabled}
{...this.props}
/>
)
}
}
};
export default withEnabledFeatures;
And I will use this as a prop in another component say
isFeatureEnabled('feature_a');
Which will return true if it exists or false if not.
My question is my isFeatureEnabled function correct?
No, you are not calling this.enabledFeatures as a method, you are trying to access a member on it. Use this.enabledFeatures(). Also, the HOC factory method is not returning the extended class.
const withEnabledFeatures = (WrappedComponent: any) => {
return class WithEnabledFeatures extends React.Component<any> {
enabledFeatures = (): Array<string> =>
(window === void 0) ? [] : window.AH.featureFlags;
isFeatureEnabled = (feature: string): boolean =>
this.enabledFeatures().includes(feature); // <-- Here, ()
render() {
return (
<WrappedComponent
enabledFeatures={this.enabledFeatures()}
isFeatureEnabled={this.isFeatureEnabled}
{...this.props}
/>
)
}
}
};
export default withEnabledFeatures;
(I also optionally compressed your code and added a best practice, void 0)
Added return inside HOC for WithEnabledFeatures as well as corrected the isFeatureEnabled return statement
const withEnabledFeatures = (WrappedComponent: any) => {
return class WithEnabledFeatures extends React.Component<any> {
enabledFeatures = (): Array<string> => {
if (window === undefined) return [];
return window.AH.featureFlags;
}
isFeatureEnabled = (feature: string): boolean => {
return this.enabledFeatures().includes(feature);
}
render() {
return (
<WrappedComponent
enabledFeatures={this.enabledFeatures()}
isFeatureEnabled={this.isFeatureEnabled}
{...this.props}
/>
)
}
}
};
export default withEnabledFeatures;

Promises in react.js, model attribute is promise instead of array

I'm trying to implement a restaurant app where a user can add dishes to a menu. The menu will be displayed in a side bar. Dish information is provided through an API. I'm having issues with the API requests/promises. I'm storing a list of the dishes in DinnerModel. I'm making the requests to the API in DinnerModel.
When I add a dish to the menu by clicking the add button in IngredientsList, I get redirected to a screen that shows Sidebar. But in Sidebar, the dishes are NaN. The console.logs show that this.state.menu in Sidebar is actually a Promise, not an array. I'm having trouble understanding why this is and what to do about it.
Note that update in Sidebar is supposed to run modelInstance.getFullMenu() which returns an array. But instead, a promise is returned. Why? What can I do to fix this?
Here's my code:
Dinnermodel.js:
const DinnerModel = function () {
let numberOfGuests = 4;
let observers = [];
let selectedDishes = [];
// API Calls
this.getAllDishes = function (query, type) {
const url = 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/search?query='+query+"&type="+type;
return fetch(url, httpOptions)
.then(processResponse)
.catch(handleError)
}
//function that returns a dish of specific ID
this.getDish = function (id) {
let url = "https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/recipes/"+id+"/information";
return fetch(url, httpOptions)
.then(processResponse)
.catch(handleError)
}
// API Helper methods
const processResponse = function (response) {
if (response.ok) {
return response.json();
}
throw response;
}
this.addToMenu = function(id, type){
var newDish = this.getDish(id).then()
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
}
//Returns all the dishes on the menu.
this.getFullMenu = function() {
return selectedDishes;
}
DishDetails.js:
class DishDetails extends Component {
constructor(props) {
super(props);
this.state = {
id: props.match.params.id,
status: "INITIAL",
type: props.match.params.type,
};
}
addToMenu (){
modelInstance.addToMenu(this.state.id, this.state.type);
this.props.history.push("/search/"+this.state.query+"/"+this.state.type);
}
componentDidMount = () => {
modelInstance.getDish(this.state.id)
.then(dish=> {
this.setState({
status:"LOADED",
ingredients: dish.extendedIngredients,
dishText: dish.winePairing.pairingText,
pricePerServing: dish.pricePerServing,
title: dish.title,
img: dish.image,
instructions: dish.instructions,
})
})
.catch(()=>{
this.setState({
status:"ERROR",
})
})
}
render() {
switch(this.state.status){
case "INITIAL":
return (
<p>Loading...</p>
);
case "ERROR":
return (
<p>An error has occurred, please refresh the page</p>
);
}
return (
<IngredientsList ingredients={this.state.ingredients} pricePerServing={this.state.pricePerServing} id={this.state.id} onButtonClick={() => this.addToMenu()}/>
<Sidebar />
);
}
}
export default withRouter(DishDetails);
Sidebar.js:
class Sidebar extends Component {
constructor(props) {
super(props)
// we put on state the properties we want to use and modify in the component
this.state = {
numberOfGuests: modelInstance.getNumberOfGuests(),
menu: modelInstance.getFullMenu(),
}
modelInstance.addObserver(this);
}
// this methods is called by React lifecycle when the
// component is actually shown to the user (mounted to DOM)
// that's a good place to setup model observer
componentDidMount() {
modelInstance.addObserver(this)
}
// this is called when component is removed from the DOM
// good place to remove observer
componentWillUnmount() {
modelInstance.removeObserver(this)
}
handleChangeGuests(event){
let noOfGuests = event.target.value;
modelInstance.setNumberOfGuests(noOfGuests);
}
// in our update function we modify the state which will
// cause the component to re-render
update() {
this.setState({
numberOfGuests: modelInstance.getNumberOfGuests(),
menu: modelInstance.getFullMenu(),
})
console.log("menu in Sidebar.js");
console.log(this.state.menu);
}
render() {
//console.log(this.state.menu);
let menu = this.state.menu.map((dish)=>
<div key={"menuitem-"+dish.id} className="menuitemwrapper">
<div className="menuitem">
<span className="dishname">{dish.title}</span>
<span className="dishprice">{dish.pricePerServing*modelInstance.getNumberOfGuests()}</span>
</div>
</div>
);
return (
<div id="sidebar-dishes">
{menu}
</div>
);
}
}
export default Sidebar;
IngredientsList.js:
class IngredientsList extends Component{
constructor(props){
super(props);
this.state = {
ingredients: props.ingredients,
pricePerServing: props.pricePerServing,
id: props.id,
noOfGuests: modelInstance.getNumberOfGuests(),
}
modelInstance.addObserver(this);
}
update(){
if(this._ismounted==true){
this.setState({
noOfGuests: modelInstance.getNumberOfGuests(),
});
}
}
componentDidMount(){
this._ismounted = true;
}
componentWillUnmount(){
this._ismounted = false;
}
render () {
return (
<button onClick={() => this.props.onButtonClick()} type="button" className="btn btn-default">Add to menu</button>
);
}
}
export default IngredientsList;
EDIT:
Changed DinneModel.addToMenu to:
this.addToMenu = function(id, type){
var newDish = this.getDish(id)
.then(()=>{
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
});
}
I still get a promise logged in the console from the console.log in Sidebar.js, and NaN in the Sidebar render.
getDish is not in your code posted, but I assume that it returns a promise. And this.getDish(id).then() also returns a promise. That’s why selectedDishes array has promises in it.
this.addToMenu = function(id, type){
var newDish = this.getDish(id).then()
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
}
To get actual newDish data, you need to use a callback function for the then.
this.addToMenu = function(id, type){
this.getDish(id).then(function (newDish) {
newDish.dishType = type;
selectedDishes.push(newDish);
notifyObservers();
});
}

Loop Repeating Twice

Im wrecking my brain trying to figure out why this is map is repeating twice.
let toolbars = toolbarState.map(( toolbarEntry, i ) => {
let curToolbar = toolbarState.keyOf( toolbarEntry ); // returns the key of the property
let customProps = {};
switch( curToolbar ){
case( "temSelectionCtrl" ):
case( "functionalCtrl" ):
case( "referenceCtrl" ):
return( // returns react component
<CtrlParent eachToolbar = { toolbarEntry }
svgState = { svgState }
customProps = { customProps }
key = { i }/>
);
case( "operationalCtrl" ):
customProps = {
enableZoomIn,
enableZoomOut,
dispatchEnableZoom
};
return (
<CtrlParent eachToolbar = { toolbarEntry }
svgState = { svgState }
customProps = { customProps }
key = { i }/>
);
}
});
To clarify, Im using immutableJs libary. Which has a method for reading the key of a value.
Also, the structure of the toolbarState looks like this:
toolbarState : {
temSelectionCtrl: {...},
functionalCtrl: {...},
operationalCtrl: {...},
referenceCtrl: {...}
}

Categories