document.getElementById is undefined in Vue2 component...because misusing watch? - html

I am making a basic chat component that loads messages from firebase into a div.
Goal is to auto-scroll to the bottom of the div WHEN relevantMessages changes, IF the current scroll location is already near
the bottom.
I tried to do this by adding a watcher to relevantMessages that calls a 'scrollToBottom' `method.
Problem is: Error in callback for watcher "relevantMessages":
"TypeError: Cannot read property 'scrollTop' of undefined"
HTML:
<template>
<div class="all">
<div class="chat-header">
...
</div>
<div class="messages-area" id="messages-area-id" ref="messagesAreaRef">
<div
v-for="(thing, index) in relevantMessages"
:key="index"
class="message-row"
>
<div class="message">
<span>{{thing.content}}</span>
<span>{{thing.timeStamp}}</span>
</div>
</div>
</div>
<div class="input-area">
...
</div>
</template>
The JS:
<script>
import firebase from "firebase";
export default {
data() {
return {
relevantMessages: Object,
partnerObj: null
};
},
computed: {
...
},
props: {
...
},
mounted: function() {
this.getMessages();
this.getPartner();
},
watch: {
relevantMessages: function() {
this.scrollToBottom();
}
},
methods: {
getMessages: function() {
var that = this;
firebase
.database()
.ref("/messages/" + that.conversationUID)
.on("value", function(messages) {
var result = messages.val();
that.relevantMessages = result;
});
console.log("getting posts");
},
sendMessage: function() {
...
},
scrollToBottom: function() {
var messagesArea = this.$refs.messagesAreaRef;
// var messagesArea = document.getElementById("messages-area-id");
var currentScrollPos = messagesArea.scrollTop;
// console.log(currentScrollPos);
var totalDivHeight = messagesArea.scrollHeight;
if (currentScrollPos <= totalDivHeight - 100) {
messagesArea.scrollTop(totalDivHeight);
}
}
}
};
The errors themselves:
vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in callback for watcher "relevantMessages": "TypeError: Cannot read property 'scrollTop' of undefined"
TypeError: Cannot read property 'scrollTop' of undefined

Perhaps the component itself is not yet mounted at this time. So all $refs are undefined. Try out the alternative syntax of watch (Documentation) in mounted(). Hope that helps!

There are a few issues with your implementation:
relevantMessages: Object,
Object does not seem to be a valid initial value for relevantMessages. You should set this to either an empty object ({}) or an empty array ([]).
messagesArea.scrollTop(totalDivHeight);
scrollTop is not a function, it should be used like messagesArea.scrollTop = totalDivHeight.
Your implementation for scrolling to the bottom is incorrect; currentScrollPos will always be less than the height of its container if it's near the top. To check if it's near the bottom, you should have currentScrollPos >= totalDivHeight - messagesArea.clientHeight - 100
Now, even with the above changes, it appears the watcher will run and execute before Vue has a chance to actually re-render and update the DOM (thus, the scrolling will always be one node behind), so you must call your scrollToBottom after the "next tick":
watch: {
relevantMessages() {
this.$nextTick(this.scrollToBottom);
}
},
I created a working example in CodeSandbox here: https://codesandbox.io/embed/vue-template-tqo4k.

Related

How get data lists from json using Vue axios infinite scroll

I would like to import the data from Vue.js to axios and apply the infinite scroll.
and want to have json data displayed in order. Now only index [0] is being repeated. How can we solve it? Thank you.
https://jsfiddle.net/naeun/eywraw8t/131773/
<div id="app">
<section data-aos="fade-zoom-in" v-for="post in posts">
<div class="post">
<p class="colon" data-aos="fade-zoom-in"><span>“</span></p>
<p class="quote">{{post.quote}}</p>
<p class="colon" data-aos="fade-zoom-in"><span>”</span></p>
<p class="author" data-aos="fade-zoom-in">{{post.author}}</p>
</div>
</section>
</div>
new Vue({
el: "#app",
data: {
bottom: false,
posts: []
},
methods: {
bottomVisible() {
const scrollY = window.scrollY;
const visible = document.documentElement.clientHeight;
const pageHeight = document.documentElement.scrollHeight;
const bottomOfPage = visible + scrollY >= pageHeight;
return bottomOfPage || pageHeight < visible;
},
addPost() {
axios.get(`https://jsonplaceholder.typicode.com/posts`)
.then(response => {
let api = response.data[0];
let apiInfo = {
author: api.id,
quote: api.title,
tag: api.body
};
this.posts.push(apiInfo)
if(this.bottomVisible()) {
this.addPost();
}
})
.catch(e => {
console.log('Error: ', error)
})
}
},
watch: {
bottom(bottom) {
if (bottom) {
this.addPost();
}
}
},
created() {
window.addEventListener('scroll', () => {
this.bottom = this.bottomVisible()
});
this.addPost();
}
})
There are a few problems here. First, whenever you scroll to the bottom, you call the addPost method, right? But the method itself doesn't know which "page" to load. It does the very same request over and over again. Which means it gets the same results each time.
Then you use this let api = response.data[0];, which means that no matter what results you receive, you only get the first item from the list and push it to your local array.
What you need to do is to keep track of the virtual "page" that you are loading, meaning that each addPost is like loading additional items from a virtual pagination and just putting them at the end of the infinite list, instead of reloading the page. Then you need to pass this parameter to the method that loads those new items and prepare the backend to return specific items based on request parameters.
Good luck!

Polymer 1.0: how to correctly bind to (inject / override) a function of a dom-module

I have a Polymer element of which the public API should allow to bind to a function which the user can define, i.e. it should allow to pass in a function implementation. I have tried quite a few approaches but only 1 worked. Now I'm wondering if this is the proper / correct way or not.
To rephrase: what is the proper way to bind a function as part of a dom-module's public API? The only way I have this achieved somewhat is as follows:
<dom-module id="channel-search">
<template>
<remote-dropdown
id="dropdown"
label-text="Type channel name"
url='{{_findRecordUrl}}'
url-transformer='{{urlTransformer}}'
result-to-list-transformer='{{resultToListTransformer}}'
class="layout horizontal"
style='width: 100%'>
</remote-dropdown>
</template>
</dom-module>
<script>
(function() {
Polymer({
is: 'channel-search',
properties: {
_findRecordUrl: {
type: String,
value: 'http://127.0.0.1:9292/epics-boot-info.psi.ch/find-channel.aspx'
}
},
/*
* Here in the attached function, I define the methods which will then be bound to
* the respective properties of the remote-dropdown element
*/
attached: function() {
this.urlTransformer = function(baseUrl, currentInput) {
return baseUrl + '/' + currentInput;
};
this.resultToListTransformer = function(findRecordList) {
var responseList = findRecordList.map(function(res) {
return res.Channel;
});
return responseList;
};
}
});
})();
</script>
So, I needed to define the functions in the attached callback in order for them to be properly bound to the remote-dropdown element's public API.
I hoped it would be a bit clearer / easier, maybe like so:
<script>
(function() {
Polymer({
is: 'channel-search',
properties: {
_findRecordUrl: {
type: String,
value: 'http://127.0.0.1:9292/find-channel.aspx'
}
},
urlTransformer: function(baseUrl, currentInput) {
return baseUrl + '/' + currentInput;
};
resultToListTransformer: function(findRecordList) {
var responseList = findRecordList.map(function(res) {
return res.Channel;
});
return responseList;
};
});
})();
</script>
i.e. simply define the function implementation as part of the element's definition and then bind those to the embedded remote-dropdown element. However, that never seemed to work as I thought it would (also not variations thereof) - surely also because of my limited knowledge of Polymer/Javascript internals.
My question is: is the solution using the attached callback the proper way to achieve what I am trying to do? If not, what would be the correct way to implement this?
You can probably do that using computed properties or computed bindings. Your first answer looks like a variation of the latter.

AngularJS: Dynamically change expression after $watch

The main thing I want to do here is:
When the form is submitted, a http get request will be done (VerbController)
On success, a JSON string is returned
I copy the JSON string into a factory object (called VerbFactory)
I want to output the content of the JSON string in a div through another controller (OutputController), I took the attribute "name" as an example here.
To achieve this (point 4), I watched for a change in the VerbFactory object and when the JSON string after requesting gets loaded into the object, I want to store it in a variable of the OutputController, so that I can make an expression for it in my HTML.
But it does not work right now. It seems that this.verb is in another scope than the controller scope. I have difficulties understand the difference between $scope and this here, even though I have read a decent amount of articles about the difference between those two.
How do I solve this problem? Do I miss something obvious?
NB: I added some jQuery that puts the attribute "name" of the JSON into a debug div, and it works as expected. But the AngularJS expression {[{outputCtrl.verb["#attributes"]["name"]}]} does not work.
HTML:
<div class="container">
<div class="row">
<div id="debug" class="col-xs-12 col-sm-12">
</div>
<div class="col-xs-12 col-sm-12" ng-controller="OutputController as outputCtrl">
{[{outputCtrl.test}]}
{[{outputCtrl.verb["#attributes"]["name"]}]}
</div>
</div>
</div>
JS:
(function() {
var app = angular.module('LG', []).config(function($interpolateProvider){
$interpolateProvider.startSymbol('{[{').endSymbol('}]}');
});
app.factory("VerbFactory", function(){
var json = {};
var available = false;
return {
getJSON: function() {
return json;
},
setJSON: function(newObj) {
angular.copy(newObj, json);
available = true;
},
isAvail: function() {
return available;
},
resetAvail: function() {
available = false;
}
};
});
app.controller("VerbController", ['$http', 'VerbFactory', function($http, VerbFactory){
this.verb = "";
this.requestVerb = function() {
VerbFactory.resetAvail();
var that = this;
$http.get('/request/' + that.verb).
success(function(data) {
VerbFactory.setJSON(data);
}).
error(function() {
});
this.verb = "";
};
}]);
app.controller("OutputController", ['$scope', 'VerbFactory', function($scope, VerbFactory){
this.test = "Test!";
$scope.$watch(VerbFactory.isAvail, function(){
this.verb = VerbFactory.getJSON();
$('#debug').append('<p>'+ this.verb["#attributes"]["name"] +'</p>');
});
}]);
})();
this inside of $scope.$watch callback refers to the callback scope, not the outer scope of OutputController. Use var self = this to refer to the OutputController.
ControllerAs Syntax
OutputController.js
var self = this
$scope.$watch(VerbFactory.isAvail, function(){
self.verb = VerbFactory.getJSON();
//etc
});
Regular Controller Syntax
OutputController.js
$scope.$watch(VerbFactory.isAvail, function() {
$scope.verb = VerbFactory.getJSON();
//etc
});

Converting Angular's $scope to (Controller as) while using $http

I'm using Angular 1.3 I have changed my code to use Controller as from using $scope.
I have a url:
http://localhost:3640/api/v1/topics
That gets the following json:
[
{
topicId: 17,
title: "This is another BRAND SPANKIN new topic",
body: "This is a message in the body of another topic ftw!",
created: "2014-11-27T05:37:49.993",
replies = null
},
{
topicId: 18,
title: "This is another BRAND new topic",
body: "This is a message in the body of a topic wow!",
created: "2014-11-27T05:37:49.993",
replies = null
}
]
I also have a page called index-home.js
var app = angular.module("myApp", []);
app.controller("homeIndexController", ["$http", function ($http) {
var msg = this;
msg.dataCount = 0;
msg.replies = [];
$http.get("/api/v1/topics?includeReplies=true")
.success(function(data) {
//Success
msg.replies = data;
})
.error(function() {
//Error
alert('error/failed');
});
}]);
On my page I use the following bindings:
<div id="ngController" ng-controller="homeIndexController as hic">
...
<h3>Message count: {{hic.dataCount}}</h3>
...
<div class="message row" data-ng-repeat="i in hic.data">
<div class="title">{{i.title}}</div>
<div class="date">{{i.created}}</div>
<div class="contents">{{i.body}}</div>
</div>
I know that the url in the $http is working because I tried it by itself in the browser and fiddler and it returned the json. But when I use it in my angular app I get the .error result (which is a alert saying FAIL.
I have tried removing the http://localhost:3640 and just using /api/v1/topics when I do that, I don't get the error result anymore. I know my controller is working and binding to the page because I get back 0 for dataCount.
What am I doing wrong in the $http method? Is my syntax wrong?
The error is in your ng-repeat attribute. You do not have a data variable in your scope.
Change hic.data to hic.replies and it will work.
<div id="ngController" ng-controller="homeIndexController as hic">
...
<h3>Message count: {{hic.dataCount}}</h3>
...
<div class="message row" data-ng-repeat="i in hic.replies">
<div class="title">{{i.title}}</div>
<div class="date">{{i.created}}</div>
<div class="contents">{{i.body}}</div>
</div>
Alright dude, this is a real rookie mistake, look at your controller:
app.controller("homeIndexController", ["$http", function ($http) {
var msg = this;
msg.dataCount = 2;
msg.replies = [];
$http.get("/api/v1/topics")
.success(function(data) {
//Success
msg.replies = data;
})
.error(function() {
//Error
alert('eror/failed');
});
}])
You are setting this to msg, then setting msg.replies to equal data if success.
This is ok.
Your problem then comes in your ng-repeat when you say:
<div data-ng-repeat="i in hic.data">
hic.data doesn't exist. data was the item returned from the .success(function()) of the $http.get. You then take that data and assign it to msg.replies. So your ng-repeat="" should look like this:
<div data-ng-repeat="i in hic.replies">
Understand? This is basic stuff... I, uh I mean you should feel really silly.

angularjs directive call function specified in attribute and pass an argument to it

I want to create a directive that links to an attribute. The attribute specifies the function that should be called on the scope. But I also want to pass an argument to the function that is determined inside the link function.
<div my-method='theMethodToBeCalled'></div>
In the link function I bind to a jQuery event, which passes an argument I need to pass to the function:
app.directive("myMethod",function($parse) {
restrict:'A',
link:function(scope,element,attrs) {
var expressionHandler = $parse(attrs.myMethod);
$(element).on('theEvent',function( e, rowid ) {
id = // some function called to determine id based on rowid
scope.$apply(function() {expressionHandler(id);});
}
}
}
app.controller("myController",function($scope) {
$scope.theMethodToBeCalled = function(id) { alert(id); };
}
Without passing the id I can get it working, but as soon as I try to pass an argument, the function is not called anymore
Marko's solution works well.
To contrast with recommended Angular way (as shown by treeface's plunkr) is to use a callback expression which does not require defining the expressionHandler. In marko's example change:
In template
<div my-method="theMethodToBeCalled(myParam)"></div>
In directive link function
$(element).click(function( e, rowid ) {
scope.method({myParam: id});
});
This does have one disadvantage compared to marko's solution - on first load theMethodToBeCalled function will be invoked with myParam === undefined.
A working exampe can be found at #treeface Plunker
Just to add some info to the other answers - using & is a good way if you need an isolated scope.
The main downside of marko's solution is that it forces you to create an isolated scope on an element, but you can only have one of those on an element (otherwise you'll run into an angular error: Multiple directives [directive1, directive2] asking for isolated scope)
This means you :
can't use it on an element hat has an isolated scope itself
can't use two directives with this solution on the same element
Since the original question uses a directive with restrict:'A' both situations might arise quite often in bigger applications, and using an isolated scope here is not a good practice and also unnecessary. In fact rekna had a good intuition in this case, and almost had it working, the only thing he was doing wrong was calling the $parsed function wrong (see what it returns here: https://docs.angularjs.org/api/ng/service/$parse ).
TL;DR; Fixed question code
<div my-method='theMethodToBeCalled(id)'></div>
and the code
app.directive("myMethod",function($parse) {
restrict:'A',
link:function(scope,element,attrs) {
// here you can parse any attribute (so this could as well be,
// myDirectiveCallback or multiple ones if you need them )
var expressionHandler = $parse(attrs.myMethod);
$(element).on('theEvent',function( e, rowid ) {
calculatedId = // some function called to determine id based on rowid
// HERE: call the parsed function correctly (with scope AND params object)
expressionHandler(scope, {id:calculatedId});
}
}
}
app.controller("myController",function($scope) {
$scope.theMethodToBeCalled = function(id) { alert(id); };
}
Not knowing exactly what you want to do... but still here's a possible solution.
Create a scope with a '&'-property in the local scope.
It "provides a way to execute an expression in the context of the parent scope" (see the directive documentation for details).
I also noticed that you used a shorthand linking function and shoved in object attributes in there. You can't do that. It is more clear (imho) to just return the directive-definition object. See my code below.
Here's a code sample and a fiddle.
<div ng-app="myApp">
<div ng-controller="myController">
<div my-method='theMethodToBeCalled'>Click me</div>
</div>
</div>
<script>
var app = angular.module('myApp',[]);
app.directive("myMethod",function($parse) {
var directiveDefinitionObject = {
restrict: 'A',
scope: { method:'&myMethod' },
link: function(scope,element,attrs) {
var expressionHandler = scope.method();
var id = "123";
$(element).click(function( e, rowid ) {
expressionHandler(id);
});
}
};
return directiveDefinitionObject;
});
app.controller("myController",function($scope) {
$scope.theMethodToBeCalled = function(id) {
alert(id);
};
});
</script>
You can create a directive that executes a function call with params by using the attrName: "&" to reference the expression in the outer scope.
We want to replace the ng-click directive with ng-click-x:
<button ng-click-x="add(a,b)">Add</button>
If we had this scope:
$scope.a = 2;
$scope.b = 2;
$scope.add = function (a, b) {
$scope.result = parseFloat(a) + parseFloat(b);
}
We could write our directive like so:
angular.module("ng-click-x", [])
.directive('ngClickX', [function () {
return {
scope: {
// Reference the outer scope
fn: "&ngClickX",
},
restrict: "A",
link: function(scope, elem) {
function callFn () {
scope.$apply(scope.fn());
}
elem[0].addEventListener('click', callFn);
}
};
}]);
Here is a live demo:
http://plnkr.co/edit/4QOGLD?p=info
Here's what worked for me.
Html using the directive
<tr orderitemdirective remove="vm.removeOrderItem(orderItem)" order-item="orderitem"></tr>
Html of the directive: orderitem.directive.html
<md-button type="submit" ng-click="remove({orderItem:orderItem})">
(...)
</md-button>
Directive's scope:
scope: {
orderItem: '=',
remove: "&",
My solution:
on polymer raise an event (eg. complete)
define a directive linking the event to control function
Directive
/*global define */
define(['angular', './my-module'], function(angular, directives) {
'use strict';
directives.directive('polimerBinding', ['$compile', function($compile) {
return {
restrict: 'A',
scope: {
method:'&polimerBinding'
},
link : function(scope, element, attrs) {
var el = element[0];
var expressionHandler = scope.method();
var siemEvent = attrs['polimerEvent'];
if (!siemEvent) {
siemEvent = 'complete';
}
el.addEventListener(siemEvent, function (e, options) {
expressionHandler(e.detail);
})
}
};
}]);
});
Polymer component
<dom-module id="search">
<template>
<h3>Search</h3>
<div class="input-group">
<textarea placeholder="search by expression (eg. temperature>100)"
rows="10" cols="100" value="{{text::input}}"></textarea>
<p>
<button id="button" class="btn input-group__addon">Search</button>
</p>
</div>
</template>
<script>
Polymer({
is: 'search',
properties: {
text: {
type: String,
notify: true
},
},
regularSearch: function(e) {
console.log(this.range);
this.fire('complete', {'text': this.text});
},
listeners: {
'button.click': 'regularSearch',
}
});
</script>
</dom-module>
Page
<search id="search" polimer-binding="searchData"
siem-event="complete" range="{{range}}"></siem-search>
searchData is the control function
$scope.searchData = function(searchObject) {
alert('searchData '+ searchObject.text + ' ' + searchObject.range);
}
This should work.
<div my-method='theMethodToBeCalled'></div>
app.directive("myMethod",function($parse) {
restrict:'A',
scope: {theMethodToBeCalled: "="}
link:function(scope,element,attrs) {
$(element).on('theEvent',function( e, rowid ) {
id = // some function called to determine id based on rowid
scope.theMethodToBeCalled(id);
}
}
}
app.controller("myController",function($scope) {
$scope.theMethodToBeCalled = function(id) { alert(id); };
}