Immutable StencilJS Props re-render when changed - stenciljs

I have the following StencilJS component with three properties.
import { Component, h, Prop } from "#stencil/core";
#Component({
tag: 'twl-button',
styleUrls: ['../style-tw.dist.css', './twl-button.scss'],
scoped: true
})
export class ButtonComponent {
#Prop() isBoolean: boolean = false;
#Prop() aNumber: number = -1;
#Prop() aString: string = "a"
componentWillLoad() {
alert(`componentWillLoad: ${this.isBoolean} | ${this.aNumber} | ${this.aString}`)
}
render() {
alert(`rendering: ${this.isBoolean} | ${this.aNumber} | ${this.aString}`)
return (
<button onClick={() => this.didClick()}class={'w-1/3 self-center flex justify-center mt-1/12 py-2 px-2 border border-transparent font-normal text-lg rounded-md text-white bg-nord9 hover:bg-nord14 focus:outline-none focus:border-nord9 focus:shadow-outline-gray active:bg-nord10 transition duration-150 ease-in-out'}>
Click to Change
</button>
);
}
didClick(): void {
this.isBoolean = !this.isBoolean;
this.aNumber = this.aNumber+1;
this.aString = this.aString+"b";
}
}
It's used in JSX as:
<twl-button isBoolean={true} aNumber={0} aString={"init-string"}></twl-button>
My understanding reading the StencilJS documentation is that these properties are implicitly immutable. I would think that an immutable property would also imply no-rendering, since nothing changes. However, when I click the button, the values change and render is called. Where is my misunderstanding?
pacakge.json:
...
"#stencil/core": "^1.17.3",
"#stencil/router": "^1.0.1",
"#stencil/sass": "^1.3.2",
...

You can think of the #Prop() decorator itself as creating a watch on the property (isBoolean, aNumber, aString in your case), and every time a value is assigned to it, the watch performs an equality check (===) on the old value and new value to determine if the value has changed (and therefore, whether a re-render is needed).
You would not see the same behavior if your prop was an object and you set a value on the object #Prop() because even though one of the values has changed, the === reference returns true since the object reference remains the same
#Prop() user: IUser = {
firstName: 'user',
lastName: 'name'
};
...
...
// does not trigger a re-render (since object reference remains unchanged)
user.firstName = 'new user';
// triggers a re-render (because you are essentially creating a new object and assigning it to the user property)
user = {
...user,
firstName = 'new user'
};

Related

Div doesn't appear after using v-if statement

<div class="min-h-screen flex" ref="mapDiv"></div>
<div class="bg-white absolute top-0 w-full overflow-y-auto"></div>
This is how it looks
Whenever I add to this div v-if
<div class="bg-white absolute top-0 w-full overflow-y-auto" v-if="settingsToggled"></div>
The div doesn't show up at all, even after being triggered to true. Screenshot:
I am returning settingsToggled in the setup function.
This is the code that triggers it:
export function AvatarControl(controlDiv, settingsToggled) {
const controlUI = document.createElement("div");
controlUI.style.width = "100px";
controlUI.style.margin = "10px"
controlUI.style.height = "100px";
controlUI.style.borderRadius = "8px"
controlUI.style.backgroundImage = "url('https://www.pngkey.com/png/full/114-1149878_setting-user-avatar-in-specific-size-without-breaking.png')"
controlUI.style.backgroundPosition = "center"
controlUI.innterHTML = "Avatar";
controlUI.style.zIndex = "9999";
controlDiv.appendChild(controlUI);
controlUI.addEventListener("click", () => {
console.log(settingsToggled)
settingsToggled = !settingsToggled
})
}
Anyone knows what might be the issue here?
Can I not modify the settingsToggled from outside this component?
In AvatarControl, settingsToggled appears to be a Boolean (based on settingsToggled = !settingsToggled), while your screenshot shows it as a ref, so I'm guessing you are passing the ref's value to AvatarControl like this:
// MyComponent.vue
new AvatarControl(controlDiv, settingsToggled.value)
But Booleans are passed by value, so the function can't modify the original variable like that. However, the function can modify the value of a ref argument:
// MyComponent.vue
new AvatarControl(controlDiv, settingsToggled)
// AvatarControl.js
export function AvatarControl(controlDiv, settingsToggled) {
const controlUI = document.createElement("div");
//...
controlUI.addEventListener("click", () => {
settingsToggled.value = !settingsToggled.value
})
}
}
You need a component data property for settingstoggled otherwise vue v-if hook will not find it.
Try like this in your .vue file:
data () {
return {
settingstoggled: true
}
}
You can find more info on component data here.

[Vue warn]: Property or method "Boston" is not defined on the instance but referenced during render

I'm setting up some props, as shown below.
Component 1 (Parent):
<template>
<div>
<span>{{agency1}}</span>
<span>{{workstation}}</span>
</div>
</template>
<script>
export default {
name: "work-station-view",
props: {
agency1: {
type: String
},
workstation: {
type: Number
}
},
data() {
return {};
}
};
</script>
Component 2 (Child):
<template>
<WorkStationView :workstation="1.1" :agency1="Boston" />
</template>
The workstation prop renders fine, but the agency1 prop doesn't show up at all. I get this message from Vue in the console:
[Vue warn]: Property or method "Boston" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
I checked the docs, as it says to define it in data(), so I did a combination of all of these (probably more) to no avail:
// attempt 1
data() {
agency1 = this.agency1;
return {};
}
// attempt 2
data() {
return {
agency1 = this.agency1;
};
}
// attempt 3
data() {
return {
agency1: '';
};
}
If I use a number value for agency1 (<WorkStationView :workstation="1.1" :agency1="3" />), it shows! What is going on?
If you're using an inline string, you should skip the : or quote your string.
The : is short-hand for v-bind and is expected to be used with variables that you're binding when passing attributes from the parent component to the child. In this case, you don't have a variable called Boston in the parent context, and hence the error from Vue.
If all you want to do is use a constant string like Boston, just use it like
<WorkstationView :workstation="1.1" :agency="'Boston'" />
Alternatively, it would've also worked if you did the following:
<WorkstationView :workstation="1.1" agency="Boston" />
:agency1="Boston" is shorthand for v-bind:agency1="Boston". It attempts to bind a data property named Boston, but you don't have one defined. :agency1="3" works because 3 is a literal. If you were attempting to assign the literal string "Boston" to agency1, don't use the preceding colon:
<!--
<WorkStationView :agency1="Boston">
--> <!-- DON'T DO THIS -->
<WorkStationView agency1="Boston">

VueJS 2 - Listen to event in mixin

I'm currently trying to create a mixin for Vue which basically creates a property passthrough chain. I'll clarify what should happen to be a little more clear;
Let's say I got 3 components; A,B and C.
A & B are both the same component called 'content-pane' (See below for template code).
<div class="pane-wrapper">
<div class="content-pane" :class="{'is-hidden' : !active}" :content="name">
<div class="card white">
<div class="card-title grey darken-3">
<h1 class="white-text">{{ label }}</h1>
</div>
<div class="card-content white">
<component
:is = "type"
:routes = "routes"
:passthrough = "passthrough"
keep-alive
></component>
</div>
</div>
</div>
<content-pane
v-for="(pane, key) in children"
:key = "key"
:label = "pane.label"
:name = "pane.name"
:active = "true"
:type = "pane.type"
:routes = "pane.routes"
></content-pane>
</div>
C is a dynamic component, meaning that it is interchangeable and could be any component.
Now I want to be able to access certain data from component C in component A, and for that I am trying to create a mixin that dyamically offers a data property to do this:
<script>
export default {
name: 'passthrough',
props: {
passthrough : {
type : Object
}
},
data ()
{
return {
// This object allows you to
// update the parent.
passthroughModifier : {
// We use the data object inside the
// original object because Vue doesn't
// want to detect direct prop changes
// when they are added dynamically
// into the root object...
data : {}
}
}
},
methods : {
/**
* This function fires an emit event.
*/
emitUpdate ()
{
this.$emit('passthrough-update', this.passthroughModifier);
}
},
watch : {
/**
* Emit an event once the passthrough
* property has been changed.
* We need to use a deep watcher.
*/
'passthroughModifier' : {
handler : function (val) {
this.emitUpdate();
},
deep: true
}
},
created ()
{
// Allow access to the instance
// inside the iteration.
let _that = this;
// Attach a listener for the passthrough update
// which will walk through all the keys in the
// data object and hard-set these locally.
this.$on('passthrough-update', function (data) {
Object.keys(data).forEach(function (index) {
_that.passthroughModifier[index] = data[index];
});
});
}
}
Everything works fine except listening to the 'passthrough-update' event, which is fired by the watcher on $.passthroughModifier.
So; When component C updates its $.passthroughModifier.data, the event gets emitted, but component B isn't able to catch this event.
I have tried to listen for this event in the created() method of the mixin (see code above), but it seems as if the event only gets caught in the component the event is fired from. So component C fires the event, and component C listens to its own event.
I hope someone is able to tell me wether this is actually possible or not, and what I'm doing wrong if it is possible.

How to trigger event in all sibling components except current component in vuejs?

I have a reusable component that does inline editing for data.
So a page has 10 fields that can be edited inline,
<editfield :value="field" v-for="field in fieldslist"></editfield>
Each of them have a data field called "editing" that sets as true or false as the user clicks on it. Everytime a field is set to editing an event editing-another-field is emitted using event bus.
edit(){
this.editing = true;
EventBus.$emit('editing-another-field');
}
I added the event listener when the component is created
created(){
EventBus.$on('editing-another-field', ()=>{ this.editing = false;});
}
The problem I am facing is it is triggering the event even in the currennt component being edited.
How can I mention that updated value of editing in all the other sibling components except the current component.
Why not pass the current component as an event argument and use that to check if the event originated from this component or another one.
edit() {
this.editing = true;
EventBus.$emit('editing-another-field', this);
}
created() {
EventBus.$on('editing-another-field', source => {
if (source !== this) {
this.editing = false;
}
});
}
Or you can do it like this (it is important to unregister the event listener when the component is destroyed to avoid a memory leak):
edit() {
EventBus.$emit('editing-field', this);
}
created() {
this.editingFieldHandler = vm => {
this.editing = vm === this;
};
EventBus.$on('editing-field', this.editingFieldHandler);
}
destroyed() {
EventBus.$off('editing-field', this.editingFieldHandler);
}
Otherwise you can emit the event first and then set this.editing to true.
Are you sure you want an event bus? This brings up bad memories of JQuery ;-) I think it would be cleaner to limit yourself to a tree of parents and children. Thinking MVVM, formLockedBy is a perfectly valid and sensible property to store on the parent and pass to the children.
The solution below, running here, shows a form with two fields. The fields are both instances of modal-component. The parent manages the formLockedBy property. The child fields look at this property to know to disable themselves. When the user starts typing in a field, the field emits an editing event and formLockedBy gets set. Similarly, when a field emits a save or cancel event, the parent clears formLockedBy and the other input(s) spring back to life.
Note the advantages...
Only the parent listens for events.
The identifier stored in formLockedBy is just the string name of the field. This is much safer than passing and storing a reference to the Vue component. If you don't like this, you might consider adding a safe id to the object proto.
No surprises. The full list of events the parent will react to is declared in the tag instantiating the child. The child specifies in props everything it needs from the parent.
HTML
<div id="example">
<modal-input name='First Name'
:form-locked-by='this.formLockedBy'
v-on:save='formLockedBy = null'
v-on:cancel='formLockedBy = null'
v-on:editing='fieldActive'
></modal-input>
<modal-input name='Address'
:form-locked-by='this.formLockedBy'
v-on:save='formLockedBy = null'
v-on:cancel='formLockedBy = null'
v-on:editing='fieldActive'
></modal-input>
</div>
JS
Vue.component('modal-input', {
template: `<div>
{{name}} :
<input :name='name' type="text" v-on:keydown="active" :disabled="formLockedBy && formLockedBy != name"/>
<span v-if="editing && formLockedBy == name">
<input type="button" value="Save" v-on:click="$emit('save');editing=false;"></input>
<input type="button" value="Cancel" v-on:click="$emit('cancel');editing=false;"></input>
</span>
</div>`,
data : function(){
return {editing:false};
},
props: ['name','formLockedBy'],
methods : {
active : function(event){
if(!this.editing){
this.editing = true;
this.$emit('editing',{field:this.name})
}
return true;
}
}
});
// create a root instance
new Vue({
el: '#example',
data: {
formLockedBy : null
},
methods : {
fieldActive : function(args){
this.formLockedBy = args.field;
}
}
})

Binding method result to v-model with Vue.js

How do you bind a method result to a v-model with Vue.js?
example :
<someTag v-model="method_name(data_attribute)"></someTag>
I can't make it work for some reason.
Thank you.
Years later, with more experience, I found out that is it easier to bind :value instead of using v-model. Then you can handle the update by catching #change.
Edit (per request):
<input :value="myValue" #change="updateMyValue">
...
methods: {
updateMyValue (event) {
myValue = event.target.value.trim() // Formatting example
}
}
And in a child component:
// ChildComponent.vue
<template>
<button
v-for="i in [1,2,3]">
#click="$emit('change', i) />
</template>
// ParentComponent.vue
<template>
<child-component #change="updateMyValue" />
</template>
<script>
import ChildComponent from './child-component'
export default {
components: {
ChildComponent
},
data () {
return {
myvalue: 0
}
},
methods: {
updateMyValue (newValue) {
this.myvalue = newValue
}
}
}
</script>
v-model expressions must have a get and set function. For most variables this is pretty straight forward but you can also use a computed property to define them yourself like so:
data:function(){
return { value: 5 }
},
computed: {
doubleValue: {
get(){
//this function will determine what is displayed in the input
return this.value*2;
},
set(newVal){
//this function will run whenever the input changes
this.value = newVal/2;
}
}
}
Then you can use <input v-model="doubleValue"></input>
if you just want the tag to display a method result, use <tag>{{method_name(data_attribute)}}</tag>
Agree with the :value and #change combination greenymaster.
Even when we split the computed property in get/set, which is help, it seems very complicated to make it work if you require a parameter when you call for get().
My example is a medium sized dynamic object list, that populates a complex list of inputs, so:
I can't put a watch easily on a child element, unless I watch the entire parent list with deep, but it would require more complex function to determine which of the innter props and/or lists changed and do what fromthere
I can't use directly a method with v-model, since, it works for providing a 'get(param)' method (so to speak), but it does not have a 'set()' one
And the splitting of a computed property, have the same problem but inverse, having a 'set()' but not a 'get(param)'