Alpine Expression Error: Cannot set properties of null (setting '_x_dataStack') - alpine.js

I created a simple countdown with Alpine JS, but when timer is 0, I have an error.
First declare in x-data the function appTimer.
<div x-data="appTimer()">
<div x-show="active">
<template x-if="countdown > 0">
<div>
<div>Counting down</div>
<div x-text="countdown"></div>
</div>
</template>
<template x-if="countdown === 0">
Countdown completed!
</template>
</div>
</div>
This code JS, here set active, countdown and window.setInterval.
<script>
function appTimer()
{
return {
active: true,
countdown: 5,
init() {
window.setInterval(() => {
if(this.countdown > 0) this.countdown = this.countdown - 1; console.log(this.countdown)}, 1000)
}
}
}
</script>

The issue is that you're putting text in the template instead of html, alpine tries to get the html defined in the template but doesn't find any so it gets confused, to fix the issue you simply need to wrap your message in an html element, such as:
<template x-if="countdown === 0">
<div>Countdown completed!</div>
</template>
see this stackblitz reproduction

I think it depends on your compare. You compare strict but i dont know if countdown is an integer. Try to compare with ==
<template x-if="countdown == 0">

Related

Framer motion new animation and exit not working with mapping

for some reason my exit and new animation is not working. I would like new animation to start every time user click on different menu link. I have also tried with " animate='visible'" , and I have tried also to put directly over motion, and it still not doing exit or starting new animation. I am using .map and framer motion together. Can someone please check it out.
This is the code
Thanks
const [forMapping, setForMapping] = useState(wines)
function menuHandler(index, alt) {
setIsActive(index)
if (alt === 'wine') {
setForMapping(wines)
} else if (alt === 'rakia') {
setForMapping(rakia)
}
}
const variants = {
visible: i => ({
y: 0,
transition: {
duration: .7
}
}),
hidden: {
y: '40%'
}
}
<AnimatePresence>
{forMapping.map((item, index) => {
const {
name,
description,
alt,
imageSrc,
price,
glass_price,
iconSrc,
alc
} = item;
return (
<motion.div
exit={{y: '100'}}
viewport={{once: true}}
custom={index}
whileInView='visible'
initial='hidden'
variants={variants}
key={index}
className='item'>
<div className="image">
<Image
width={200}
height={400}
objectFit='cover'
src={imageSrc}
alt={alt}/>
</div>
<div className="info">
<div className="info-header">
<header>
{name}
</header>
<p className="price">
{price.toFixed(2)} EUR
</p>
</div>
<p className="description">
{description}
</p>
<div className="bottom">
<p>
{alc} %VOL
</p>
<div className='image-price'>
<Image
width={18}
height={20}
objectFit='cover'
src={iconSrc}
alt='wine glass'/>
<p className="black">
{glass_price.toFixed(2)} EUR
</p>
</div>
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
You should not use the loop index as the key in your motion.div. Since the indices (and thus the keys) will always be the same, it doesn't let <AnimatePresence> track when elements have been added or removed in order to animate them.
Instead use a value like a unique id property for each element. This way React (and AnimatePresence) will know when it's rendering a different element (vs the same element with different data). This is what triggers the exit and enter animations.
Here's a more thorough explanation:
react key props and why you shouldn’t be using index

AlpineJS: How to pass a x-for variable to an x-data function

could you please help?
I 'm trying to add a count down according to some property in a loop but I could not find any way (not by trying out nor by googling) how I could pass that value in my functions:
<template x-for="item in cartData.items">
[...]
<template x-if="item.product_type == 'test'">
<div x-data="getCountdown()" x-init="init()">
<span x-text="timeLeft(item.timerEnd)"></span>
</div>
<script type="text/javascript">[...]
</script>
</template>
</template>
I was trying to pass item.timerEnd to every functon (getCountdown, init and timeLeft) but I always get the error that item is undefined, wheras if I pass it eg. to
<span x-text="new Date(item.timerEnd).toLocaleString()"></span> this works.
What am I missing?
PS: Thanks fpr the first help here: How to make timer in alpine.js app with time interval
Do this $data.item.timerEnd.
Like this:
<template x-for="item in cartData.items">
[...]
<template x-if="item.product_type == 'test'">
<div x-data="getCountdown($data.item.timerEnd)" x-init="init($data.item.timerEnd)">
<span x-text="timeLeft($data.item.timerEnd)"></span>
</div>
<script type="text/javascript">[...]
</script>
</template>
</template>

Passing the Div id to another vue component in laravel

I created a simple real-time chat application using vue js in laravel.
I am having a problem with the automatic scroll of the div when there is a new data.
What I want is the div to automatically scroll down to the bottom of the div when there is a new data.
Here is my code so far.
Chat.vue file
<template>
<div class="panel-block">
<div class="chat" v-if="chats.length != 0" style="height: 400px;" id="myDiv">
<div v-for="chat in chats" style="overflow: auto;" >
<div class="chat-right" v-if="chat.user_id == userid">
{{ chat.chat }}
</div>
<div class="chat-left" v-else>
{{ chat.chat}}
</div>
</div>
</div>
<div v-else class="no-message">
<br><br><br><br><br>
There are no messages
</div>
<chat-composer v-bind:userid="userid" v-bind:chats="chats" v-bind:adminid="adminid"></chat-composer>
</div>
</template>
<script>
export default {
props: ['chats','userid','adminid'],
}
</script>
ChatComposer.vue file
<template>
<div class="panel-block field">
<div class="input-group">
<input type="text" class="form-control" v-on:keyup.enter="sendChat" v-model="chat">
<span class="input-group-btn">
<button class="btn btn-primary" type="button" v-on:click="sendChat">Send Chat</button>
</span>
</div>
</div>
</template>
<script>
export default{
props: ['chats','userid','adminid'],
data() {
return{
chat: ''
}
},
methods: {
sendChat: function(e) {
if(this.chat != ''){
var data = {
chat: this.chat,
admin_id: this.adminid,
user_id: this.userid
}
this.chat = '';
axios.post('/chat/sendChat', data).then((response) => {
this.chats.push(data)
})
this.scrollToEnd();
}
},
scrollToEnd: function() {
var container = this.$el.querySelector("#myDiv");
container.scrollTop = container.scrollHeight;
}
}
}
</script>
I am passing a div id from the Chat.vue file to the ChatComposer.vue file.
As you can see in the ChatComposer.vue file there is a function called scrollToEnd where in it gets the height of the div id from Chat.vue file.
When the sendchat function is triggered i called the scrollToEnd function.
I guess hes not getting the value from the div id because I am getting an error - Cannot read property 'scrollHeight' of null.
Any help would be appreciated.
Thanks in advance.
the scope of this.$el.querySelector will be limited to only ChatComposer.vue hence child component can not able to access div of parent component #myDiv .
You can trigger event as below in ChatComposer
this.$emit('scroll');
In parent component write ScollToEnd method and use $ref to assign new height
<chat-composer v-bind:userid="userid" v-bind:chats="chats" v-bind:adminid="adminid" #scroll="ScollToEnd"></chat-composer>
..

How to display only single validation error at a time

I have this code to which displaying errors on my form
<input [ngFormControl]="form1.controls['thing']" type="text" id="thing" #thing="ngForm">
<div *ngIf='thing.dirty && !thing.valid'>
<div class="err" *ngIf='thing.errors.required'>
Thing is required.
</div >
<div class="err" *ngIf='thing.errors.invalid'>
Thing is invalid.
</div >
</div>
But in case of thing has two errors in it the two error show up.
Lets say if my input has 5 validators so 5 divs will show up which is not nice.
How to display just one error div at a time?
You could create a custom pipe to get the first element of the errors object of the validator:
#Pipe({
name: 'first'
})
export class FirstKeyPipe {
transform(obj) {
var keys = Object.keys(obj);
if (keys && keys.length>0) {
return keys[0];
}
return null;
}
}
This way you would be able to display only one error:
#Component({
selector: 'my-app',
template: `
<form>
<input [ngFormControl]="form.controls.input1">
<div *ngIf="form.controls.input1.errors">
<div *ngIf="(form.controls.input1.errors | first)==='required'">
Required
</div>
<div *ngIf="(form.controls.input1.errors | first)==='custom'">
Custom
</div>
</div>
</form>
`,
pipes: [ FirstKeyPipe ]
})
export class MyFormComponent {
constructor(private fb:FormBuilder) {
this.form = fb.group({
input1: ['', Validators.compose([Validators.required, customValidator])]
});
}
}
See this plunkr: https://plnkr.co/edit/c0CqOGuzvFHHh5K4XNnA?p=preview.
Note: agreed with Günter to create a usable component ;-) See this article for more details:
http://restlet.com/blog/2016/02/17/implementing-angular2-forms-beyond-basics-part-2/
If you have consistent markup for your error message blocks, then you can use css to display only the first message and hide the rest:
css
.message-block .error-message {
// Hidden by default
display: none;
}
.message-block .error-message:first-child {
display: block;
}
markup
<div class="message-block">
<span class="error-message" *ngIf="myForm.get('email').hasError('required')">
Email is required (first-child of message block is displayed)
</span>
<span class="error-message" *ngIf="myForm.get('email').hasError('email')">
Invalid email format (error message hidden by default)
</span>
</div>
<input [ngFormControl]="form1.controls['thing']" type="text" id="thing" #thing="ngForm">
<div *ngIf='thing.dirty && !thing.valid'>
<div class="err" *ngIf='thing.errors.required'>
Thing is required.
</div >
<div class="err" *ngIf='!thing.errors.required && thing.errors.ivalid'>
Thing is invalid.
</div >
</div>
You could create a reusable component for showing errors so you don't need to repeat this code again and again.
This works and you don't have to hardcode the validations in you template like #Joes answer above.
Template:
<input id="password" placeholder="Password" type="password" formControlName="password" [(ngModel)]="password" [ngClass]="{'invalid-input': !formUserDetails.get('password').valid && formUserDetails.get('password').touched}">
<div class="validation-container">
<ng-container *ngFor="let validation of userValidationMessages.password">
<div class="invalid-message" *ngIf="formUserDetails.get('password').hasError(validation.type) && formUserDetails.get('password').touched">
{{validation.message}}
</div>
</ng-container>
</div>
CSS:
.validation-container div {
display: none;
}
.validation-container div:first-child {
display: block;
}
Angular2 behind the scene checks the status of the control and reacts accordingly. So if you don't want to have more validation at a time, you can logically play with AND(&&) or/and OR(||) or/and NOT(!) operators.
You can create a Custom Pipe that checks first error equals with specified error:
CUSTOM PIPE
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'equals'
})
export class Equals implements PipeTransform {
transform(errors: any, error: any, args?: any): any {
if (!errors)
return false;
const array = Object.keys(errors);
if (array && array.length > 0)
return errors[array[0]] === error;
return false;
}
}
You can have lots of error div but just one error will be shown:
// input is form.controls.input1
<div *ngIf="input.errors | equals:input.errors.required">Required</div>
<div *ngIf="input.errors | equals:input.errors.maxlength">MaxLength</div>
<div *ngIf="input.errors | equals:input.errors.pattern">Pattern</div>

change client data transforms value in meteor.js

In my current meteor.js project, user can create a project and add data nodes to it. I'm using D3 to display the nodes in force graph. When they click a particular node from the graph, the corresponding text in the side panel must be highlighted. For this, I need to track with node is selected. But, I don't want to store a "selected" field on the database.
I'm using this data transform to add selected field right now -
/lib/routes.js
Router.route('/project/:code', {
name: 'projectPage',
data: function() {
return {
project: Projects.findOne({code : this.params.code}),
nodes: Nodes.find({project: this.params.code}, {transform: function (doc) {
doc.selected = false;
return doc;
}})
}
}
});
The template is /client/templates/projectPage.html
<template name="projectPage">
<div class="project-page page">
<h3>{{project.title}}</h3>
<p>{{project.summary}}</p>
<div class="work-area">
<div class="map-space">
{{> nodeDisplay nodes=nodes}}
</div>
<div class="type-space">
{{> typeDisplay nodes=nodes}}
</div>
</div>
</div>
</template>
<template name="nodeDisplay">
<div id="svgdiv"></div>
</template>
<template name="typeDisplay">
{{#each nodesData}}
<p>{{text}}</p>
<br/>
{{/each}}
</template>
The click event is handled /client/js/projects.js
Template.nodeDisplay.events({
'click .node':function(event, template){
/*remove previous selection*/
d3.selectAll('.selected circle').attr("r",32);
d3.selectAll('.selected').each(
function(d){
d.fixed = false;
d3.select(this)
.classed('selected', false);
}
);
/*add new selections*/
d3.select(event.currentTarget)
.classed("selected", true)
d3.selectAll('.selected circle').attr("r",40);
var selected_id = $(event.currentTarget).data("id");
Nodes.update(selected_id.toString(), {$set: {selected: true}});
}
});
However, this updates the database to include the "selected" field.
Is there a better way to do this and keep reactivity?
The meteor way is to use session variables and helper functions.
So instead of
Nodes.update(selected_id.toString(), {$set: {selected: true}});
use
Session.set("selected_node", this._id);
and an accompnying helper in Template.typeDisplay.helpers
isNodeSelected: function() {
if(Session.get("selected_node") === this._id) {
return "selected"
}
}
in the template displaying each node (this code assumes that you want to select the corresponding text in the typeDisplay by applying the classname 'selected'):
<template name="typeDisplay">
{{#each nodesData}}
<p class="{{isNodeSelected}}">{{text}}</p>
<br/>
{{/each}}
</template>

Resources