I have a chat component that has a multiline textbox and the chat room where the messages goes, the multiline textbox starts with a single line with 44px of height, when the user types its grows and the chat room get smaller as seen in the images below.
I get that behavior with the next code
//chat room in the chat-room i use css variables to calculate the height
//template
<vue-perfect-scrollbar ref="chatRoomScrollbar" :style="css">
<div v-viewer="viewerOptions" class="chat-room px-3 py-3">\
...
</div>
</vue-perfect-scrollbar>
//script
props: {
textFieldSize: {
type: Number,
default: 44,
},
},
computed: {
css() {
return {
'--height': `calc(310px - ${this.textFieldSize}px)`,
'--max-height': `calc(310px - ${this.textFieldSize}px)`,
};
},
},
//style
.chat-room-container > .ps-container {
height: var(--height);
min-height: var(--max-height);
background-color: #eceff1 !important;
}
and i get textFieldSize in the parent component when the #input event is fired
//chat-room
<chat-room
:text-field-size="textFieldSize">
</chat-room>
// textbox
<v-text-field
id="messageField"
v-model="form.text"
#input="getTextboxSize">
<v-icon slot="append-icon">send</v-icon>
</v-text-field>
// script
data() {
return {
textFieldSize: 44,
};
},
methods: {
getTextboxSize() {
if (document.getElementById('messageField')) {
const height = document.getElementById('messageField').offsetHeight;
this.textFieldSize = height;
this.updateChatScrollbar = true;
}
if (this.form.text === '') {
this.textFieldSize = 44;
}
},
},
when the user types, i get the height of the textfield, and pass it to the chat room and with css variable i get the height difference.
THE PROBLEM
When i select the text and delete it, or i use ctrl+z the value of textFieldSize is not beign recalculated until i type again, i have tried to use the input event in the text field, using a watcher in the form.text but none of them worked, how can make this work?
In the images below i have selected the text, then delete it, and show the textFieldSize value does not change
I would bring up 2 points:
Why are you not using $refs in getTextboxSize() - I don't think it is the cause of your issue, but it certainly could be. It's just weird to see you jump into raw js when you could set a ref="message-field" on the v-text-field element and then access the element directly, the Vue way, using this.$refs['message-field']. I'm probably off with this, but whenever I have these kinds of issue with a loss of reactivity, it's usually due to something like this.
This is cheap and not usually best practice, but can you try putting this.$forceUpdate() at the end of your getTextboxSize() function. This is a way to tell Vue to update the layout again. I would try it, and if it solves the issue, then you know it's a type of reactivity/race-condition issue. I don't ship production code with this.$forceUpdate() because if you need to use it it's usually a sign of a more fundamental design issue.
The problem is than Ctrl+Z does not trigger an input event. I think you should better not bind to input event of a textbox, but wach a form.text property for changes.
watch:{
'form.text':function(){
getTextBoxSize();
}
}
Related
Maybe for someone, this question will sound a little dumb. For me, is an unexpected behavior because I reuse a child component and I found that the data inside it are not updated when I change the source from parent. Because I don't want to implement ngOnChanges there (I try to avoid using it how much I can), an idea appear in my mind: use a boolean to hide and render the template again. But because the process is too fast, this will not emit. So, I need to use a setTimeout there (100ms are enough). But is this approach ok? This is my question. I tried to use changeDetectionRef, but still don't work, because inside the child component, all the logic are inside ngOnInit method, so will be fired only when the component is initialized.
<ng-container
*ngIf="selectedView && !reloadChart"
[ngSwitch]="selectedView.chartType">
<app-line-chart
*ngSwitchCase="'line'"
[data]="selectedView.data"
fxFlex="100"></app-line-chart>
<app-pie-chart
*ngSwitchCase="'pie'"
[data]="selectedView.data"
fxFlex="100"></app-pie-chart>
<fa-icon style="position: absolute; top: 4px; left: 4px;"
class="views__item__ok"
[icon]="faCheck"
(click)="selectView(view)"
size="2x"></fa-icon>
</ng-container>
Because selectedView get same chartType, it will not update the chart data, so inside the selectView method, I added timeout:
selectView(view: View): void {
this.reloadChart = true;
this.widget.viewId = view.id;
this.widget.requestType = view.requestType;
this.selectedView = view;
// stuff
// this will force to render again the child component,
// because for 100ms, the reloadChart field will be true
setTimeout(() => {
this.reloadChart = false;
this.cd.detectChanges();
}, 100);
}
Inside visualization panel is the problem (code displayed above). When I select one of the widgets from the carousel, it will change the child component to selectedWidget.chartType (app-pie-chart, app-line-chart, app-table-widget). The problem is when previous chart have same type as current chart, because inside the [ngSwitch] will remain same case. I don't want to modify the child component, but want to render it again inside the parent.
Here is a very simple example. I do not want to create a newline in textarea when I press enter. That's the reason why I invoke preventDefault in my handler. I also modify the value inside of textarea. The problem is the model does not get updated because I used preventDefault.
How can I prevent Enter from typing newline and make Vue to see changes in textarea?
Note, that I used el.value (update element) instead of this.value (Vue model), because I want my cursor to stay on the same position. If I update a model data, the cursor will be reset to the end after rendering view and I can't fix it's position that easy.
I will need a kind of a hack with $nextTick to fix cursor position:
this.$nextTick(() => {
el.setSelectionRange(cursorPos, cursorPos);
})
So I do not want to make things complex and do not want to touch model and cause view to be re-rendered. Why? I just want to make Vue to notice changes of input's value. I feel like I need to emit some Event manually (trigger input event for my textarea, but how?)
new Vue({
el: '#app',
data: {
value: 'Hello.World'
},
methods: {
fixGreeting(evt) {
evt.preventDefault();
let el = evt.target;
let cursorPos = el.selectionStart;
el.value = el.value.replace('.', ' ');
el.setSelectionRange(cursorPos, cursorPos);
}
}
});
<script src="https://unpkg.com/vue#2.5.17/dist/vue.js"></script>
<div id="app">
<textarea v-on:keydown.enter="fixGreeting" v-model="value"></textarea> Model: {{ value }}
</div>
Well, I think I found a solution which is clean enough.
At the end of my method code, I just need to add:
el.dispatchEvent(new Event('input'));
And Vue will change model according to element value.
Please let me know in the comments if there are any cons.
I am forcing element to be focused like this
/**focusing the element if the element is active */
componentDidUpdate() {
console.log(this.activeElementContainer);
if(this.activeElementContainer!==undefined && this.activeElementContainer!==null) {
/** need to focus the active elemnent for the keyboard bindings */
this.activeElementContainer.focus();
}
}
My render has conditional rendering the elements are being rendered dynamically from the array,
Let say I have one element in div and I am adding another from the toolbox. In that case I need to focus the last element I dragged.
render() {
let childControl= <span tabIndex="-1" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>;
if(this.props.activeItem){
childControl=<span ref={ (c) => this.activeElementContainer = c }
tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
}
//later I ma using childControl to array and it works fine.
The logs says, first time it works fine
But, second time the this.activeElementContainer is undefined
Is there any alternative way or possible solution to this?
The thing is I need to focus only one element at the time.
Remember: Activecontrol has too many things to do like it can have right click menu, drag etc. so, I need to render it separately.
After reading this one github:
This is intended (discussed elsewhere) but rather unintuitive
behavior. Every time you render:
<Value ref={(e) => { if (e) { console.log("ref", e); }}} /> You are
generating a new function and supplying it as the ref-callback. React
has no way of knowing that it's (for all intents and purposes)
identical to the previous one so React treats the new ref callback as
different from the previous one and initializes it with the current
reference.
PS. Blame JavaScript :P
Source
I changed my code to
<span id={this.props.uniqueId} ref={(c)=>
{if (c) { this.activeElementContainer=c; }}
} tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
Adding if was the real change. now, it has a ref.
For others who face this problem:
I need to write custom function too, in the componentDidUpdate I am still getting old reference,
ref={(c)=>{if (c) { this.activeElementContainer=c; this.ChangeFocus(); }}
Adding this was the perfect solution for me
I am using Template.registerHelper to register a helper that would given some boolean value will output either class1 or class2 but also some initial class if and only if it was the first time it was called for this specific DOM element.
Template.registerHelper('chooseClassWithInitial', function(bool, class_name1, class_name2, initial) {
var ifFirstTime = wasICalledAlreadyForThisDOMElement?;
return (ifFirstTime)?initial:"" + (bool)?class_name1:class_name2;
});
I am having a hard time figuring out how to know if the helper was called already for this specific form element.
If I could somehow get a reference to it, I could store a flag in the data attribute.
Using Template.instance() one can get to the "template" instance we are now rendering and with Template.instance().view to the Blaze.view instance, however, what if we have more than one html element inside our template ?
Oh, you are doing it in the wrong direction.
If you want to manipulate the DOM, you should do it directly in the template, not the jquery way ;)
0. Helper
html
<template name="foo">
<div data-something="{{dataAttributeValue}}"></div>
</template>
js
Template.foo.helpers({
dataAttributeValue: function() {
return 'some-value';
}
})
If you cannot avoid accessing the DOM from outside, then there is Template.onRendered(callback), callback will be called only once, when the template is rendered for the first time.
1. Component style
<template name="fadeInFadeOut">
<div class="fade">{{message}}</div>
</template>
Template.onRendered(function() {
// this.data is the data context you provide
var self = this,
ms = self.data.ms || 500;
self.$('div').addClass('in'); // self.$('div') will only select the div in that instance!
setTimeout(function() {
self.$('div').removeClass('in')
self.$('div').addClass('out')
}, ms );
});
Then you can use it somewhere else:
<div>
{{>fadeInFadeOut message="This message will fade out in 1000ms" ms=1000 }}
</div>
So you would have a reusable Component..
The way I solved it for now was to manually provide some kind of global identifier, unique to that item, this is hardly the proper way, if anyone has suggestions let me know.
let chooseClassWithInitialDataStore = {};
Template.registerHelper('chooseClassWithInitial', function(bool, class_name1, class_name2, initial, id) {
if(!chooseClassWithInitialDataStore[id]){
chooseClassWithInitialDataStore[id] = true;
return initial;
}
return (bool)?class_name1:class_name2;
});
To be used like:
<div class="ui warning message lowerLeftToast
{{chooseClassWithInitial haveUnsavedChanges
'animated bounceInLeft'
'animated bounceOutLeft'
'hidden' 'profile_changes_global_id'}}
">
Unsaved changes.
</div>
Regarding this specific usage: I want to class it as 'animated bounceInLeft' haveUnsavedChanges is true, 'animated bounceOutLeft' when its false, and when it is first rendered, class it as 'hidden' (that is, before any changes happen, so that it doesnt even display when rendered, thus, the need for the third option, however this isnt a question about CSS, but rather about Meteor templateHelpers).
I am using ReactCSSTransitionGroup to do some animation and I found an interesting thing which does not make any sense to me.
In the example below, when I click <div className="HeartControl">, it will update the height of the <div className="HeartFill"> which works fine. (I know to achieve the effect does not necessarily need ReactCSSTransitionGroup here though).
Interesting thing is that when I click, there will be another <div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div> with a new React component id added after the existing one.
But I expect there will always be only ONE <div className="HeartFill"> there.
Why this happened???
P.S.. after a few clicks, the result will look like:
<span data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2">
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3.5999999999999996px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$6:0"></div>
<div class="HeartFill HeartFill-enter HeartFill-enter-active" style="position:absolute;bottom:0;left:0;width:30px;height:3px;background-color:#D64541;" data-reactid=".0.4.$8de89f4f1403aee7a963122b06de3712.3.0.0.2.$=1$5:0"></div>
var HEIGHT_HEART = 30;
var NUM_HEART_MAX = 50;
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var Heart = React.createClass({
getInitialState: function() {
return {
heartHeight: 0
};
},
onClick: function(e) {
var currentHeartHeight = this.state.heartHeight;
this.setState({
heartHeight: currentHeartHeight + 1
});
},
render: function() {
var styleHeartFill = {
'position': 'absolute',
'bottom': 0,
'left': 0,
'width': 30,
'height': this.state.heartHeight / NUM_HEART_MAX * HEIGHT_HEART,
'background-color': '#D64541'
};
return (
<div className="Heart" >
<div className="HeartControl" onClick={this.onClick}>
<i className="fa fa-angle-up" />
</div>
<img src="heart.png" className="HeartOutline" />
<ReactCSSTransitionGroup transitionName="HeartFill">
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
</ReactCSSTransitionGroup>
</div>
);
}
});
React.renderComponent(<Heart />, document.getElementById('Heart'));
`
I suspect the reason your getting more than one is because your using the key prop
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
From React docs http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
When React reconciles the keyed children, it will ensure that any child with key will be reordered (instead of clobbered) or destroyed (instead of reused).
Heres a jsfiddle using the key prop http://jsfiddle.net/kb3gN/3826/
Heres a jsfiddle not using the key prop http://jsfiddle.net/kb3gN/3827/
P.s I've made a few changes in the fiddle just to try and better demostrate the reasoning
I'm fairly late to the game with this answer, but I ran into this issue as well and want to provide a solution for others. Removing the key is not a sufficient solution since React relies on it to know when to animate items. The documentation now has a section discouraging this.
You must provide the key attribute for all children of
ReactCSSTransitionGroup, even when only rendering a single item. This
is how React will determine which children have entered, left, or
stayed.
- https://facebook.github.io/react/docs/animation.html
If you are only animating the entry/exit of a single item, a CSS hack can be used to fix flickering that may be seen from multiple items entering/exiting.
.HeartFill {
display: none;
}
.HeartFill:first-child {
display: block;
}
React will add new elements on top in most cases, but this isn't guaranteed. If your transitionEndTimeout prop is set to a relatively short time, this shouldn't be a huge concern. The timeout prop should also match the CSS transition time.
Here is the problem:
You are providing a value for key which is changing over time. Keys are used to decide if an element is the same or different.
<div key={this.state.heartHeight} className="HeartFill" style={styleHeartFill}></div>
When you do this the value for key changes and React thinks a new element is entering and an old element is leaving.
Usually you need a unique key which can either be sequential or be generated using Math.random(). (remember to generate them once with getInitialState or DefaultProps, not in render, as that would create a new key every time).
The order of elements is another thing that can be in trouble.
From React's documentation:
In practice browsers will preserve property order except for properties that can be
parsed as a 32-bit unsigned integers. Numeric properties will be ordered sequentially
and before other properties. If this happens React will render components out of
order. This can be avoided by adding a string prefix to the key:
ReactCSSTransitionGroup should create a second element
it will remove it when a specified css animation has finished
or print a warning when there is no animation in the css
maybe look at the Low-level API for better understanding: http://facebook.github.io/react/docs/animation.html (bottom of the page)
I used gluxon's advice as a starting point - what worked for me was removing the leave transition and making it display nothing:
.example-leave.example-leave-active { display: none; }