While following the emberjs tutorial I couldn't get my component to pass the testing
(https://guides.emberjs.com/release/tutorial/simple-component/).
The tutorial expected the each helper to be placed in the
template file (app/templates/rentals.hbs) to send each element of the array. Instead of placing the each helper in the template file I placed it in the component file
(app/templates/component/rental-listing.hbs) so I wouldn't have to write it out each time I needed to place the rental list.
The error in testing was:
Promise rejected during "should display rental details": Cannot read property 'textContent' of null
This works in testing:
<article class="listing">
<a
onclick={{action "toggleImageSize"}}
class="image {{if this.isWide "wide"}}"
role="button"
>
<img src={{this.rental.image}} alt="">
<small>View Larger</small>
</a>
<div class="details">
<h3>{{this.rental.title}}</h3>
<div class="detail owner">
<span>Owner:</span> {{this.rental.owner}}
</div>
<div class="detail type">
<span>Type:</span> {{this.rental.category}}
</div>
<div class="detail location">
<span>Location:</span> {{this.rental.city}}
</div>
<div class="detail bedrooms">
<span>Number of bedrooms:</span> {{this.rental.bedrooms}}
</div>
</div>
</article>
While wrapping the previous code with this doesn't:
{{#each #rentalListings as |rental|}}
{{/each}}
Here's the test:
// #rental is correctly renamed to #rentalListings
module('Integration | Component | rental-listing', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.rental = EmberObject.create({
image: 'fake.png',
title: 'test-title',
owner: 'test-owner',
type: 'test-type',
city: 'test-city',
bedrooms: 3
});
});
test('should display rental details', async function(assert) {
await render(hbs`<RentalListing #rental={{this.rental}} />`);
assert.equal(this.element.querySelector('.listing h3').textContent.trim(), 'test-title', 'Title: test-title');
assert.equal(this.element.querySelector('.listing .owner').textContent.trim(), 'Owner: test-owner', 'Owner: test-owner');
});
test('should toggle wide class on click', async function(assert) {
await render(hbs`<RentalListing #rental={{this.rental}} />`);
assert.notOk(this.element.querySelector('.image.wide'), 'initially rendered small');
await click('.image');
assert.ok(this.element.querySelector('.image.wide'), 'rendered wide after click');
await click('.image');
assert.notOk(this.element.querySelector('.image.wide'), 'rendered small after second click');
});
});
Should I not be placing an each helper in a component or is my testing file not setup correctly?
I can try to create a twiddle if the problem is not clear enough.
Rentallistings you passed in each helper what's the computed value. It should be an array but what's being passed in test rentals is an object. So it will not run over an object hence no output
Related
I am fetching some data using Apollo inside of Nuxt. Somehow, when navigating to that page I get an error of
Cannot read property 'image' of undefined
When I refresh the page, everything works as expected.
I have a found a few threads of people having similar issues but no solution seems to work for me :/
This is my template file right now:
/products/_slug.vue
<template>
<section class="container">
<div class="top">
<img :src="product.image.url"/>
<h1>{{ product.name }}</h1>
</div>
</section>
</template>
<script>
import gql from 'graphql-tag'
export default {
apollo: {
product: {
query: gql`
query Product($slug: String!) {
product(filter: { slug: { eq: $slug } }) {
slug
name
image {
url
}
}
}
`,
prefetch({ route }) {
return {
slug: route.params.slug
}
},
variables() {
return {
slug: this.$route.params.slug
}
}
}
}
}
</script>
Basically the $apolloData stays empty unless I refresh the page. Any ideas would be much appreciated
EDIT
Got one step closer (I think). Before, everything (image.url and name) would be undefined when navigating to the page for the first time.
I added:
data() {
return {
product: []
};
}
at the top of my export and now at least the name is always defined so if I remove the image, everything works as expected. Just the image.url keeps being undefined.
One thing I noticed (not sure how relevant) is that this issue only occurs using the , if I use a normal a tag it works but of course takes away the vue magic.
EDIT-2
So somehow if I downgrade Nuxt to version 1.0.0 everything works fine
I stumbled on this issue as well, and found it hidden in the Vue Apollo documents.
Although quite similar to the OP's reply, it appears the official way is to use the "$loadingKey" property.
It's quite confusing in the documents because there are so many things going on.
https://vue-apollo.netlify.com/guide/apollo/queries.html#loading-state
<template>
<main
v-if="!loading"
class="my-8 mb-4"
>
<div class="w-3/4 mx-auto mb-16">
<h2 class="mx-auto text-4xl text-center heading-underline">
{{ page.title }}
</h2>
<div
class="content"
v-html="page.content.html"
></div>
</div>
</main>
</template>
<script>
import { page } from "~/graphql/page";
export default {
name: 'AboutPage',
data: () => ({
loading: 0
}),
apollo: {
$loadingKey: 'loading',
page: {
query: page,
variables: {
slug: "about"
}
},
}
}
</script>
If you need to use a reactive property within vue such as a slug, you can do so with the following.
<template>
<main
v-if="!loading"
class="my-8 mb-4"
>
<div class="w-3/4 mx-auto mb-16">
<h2 class="mx-auto text-4xl text-center heading-underline">
{{ page.title }}
</h2>
<div
class="content"
v-html="page.content.html"
></div>
</div>
</main>
</template>
<script>
import { page } from "~/graphql/page";
export default {
name: 'AboutPage',
data: () => ({
loading: 0
}),
apollo: {
$loadingKey: 'loading',
page: {
query: page,
variables() {
return {
slug: this.$route.params.slug
}
}
},
}
}
</script>
I think it's only a problem of timing on page load.
You should either iterate on products, if you have more than one, or have a v-if="product != null" on a product container, that will render only once the data is fetched from GraphQL.
In that way you'll use the object in your HTML only when it's really fetched and avoid reading properties from undefined.
To fix this, you add v-if="!$apollo.loading" to the HTML container in which you're taying to use a reactive prop.
I am having problem while trying to update the model values, when PendingActionController.updateStage method is called I need it to update the related model & reflect the updated values. If I create another method in PendingController like ShowMessage it displays the alert.
Please explain What approach should I use?
For example, following is the code:
<script type="text/x-handlebars" id="pending/_actions">
<div class="content-actions">
<h2>Pending Actions</h2>
<ul>
{{#each pendingstages}}
<li>
{{#unless refreshingStage}}
{{render 'pendingAction' this}}
{{/unless}}
</li>
{{/each}}
</ul>
</div>
</script>
<script type="text/x-handlebars" id="pendingAction">
<div class="actionsBox">
<div class="actionsBar">
<div {{bindAttr class=":actionStatus completed:blue:green"}} {{action updateStage this}}> </div>
</div>
<div class="clear-both"></div>
</div>
</script>
PendingController:
App.PendingController = App.BaseObjectController.extend(App.ActionsControllerMixin, {
needs: ['application'],
postRender: function () {
//Some code here....
},
pendingstages: function(){
return App.PendingStage.find({Id: this.get('model.id')});
}.property('model.id', 'model.#stages.completed', 'refreshStage'),
ShowMessage: function(){
alert('Inside Sohw message.');
},
});
PendingActionController
App.PendingActionMixin = {
isEditing: false,
canDelete: true,
canEdit: true,
toggleIsEditing: function(){
this.toggleProperty('isEditing');
}
};
App.PendingActionController = App.BaseObjectController.extend(App.PendingActionMixin, {
needs: 'pending',
postRender: function(){
//some code here...
},
updateStage: function(stage){
var self = this;
this.get('controllers.pending').send('pendingstages');
},
});
EDIT (1):
Followignt are the versions of Ember & ember-data:
ember-1.0.0-master.js
ember-data-master.js: CURRENT_API_REVISION: 12
Problem can be solved by using store.fetch instead of store.find.
store.fetch always calls the API, whether that particular data exists in local ember-data store or not. Use it like this..
pendingstages: function(){
return App.PendingStage.fetch({Id: this.get('model.id')});
}.property('model.id', 'model.#stages.completed', 'refreshStage'),
See ember-data/store.js code. It is deprecated now. But you'll find new methods instead of this.
I have created a dialog box using the ember-modal-dialog. The content that is going to displayed in the dialog is received from the server. I am able to make the call to server and fetch the data. But I don't know how to save the data into my model store from actions.
Controller.js
actions:{
fiModal1: function(photo){
Ember.$('body').addClass('centered-modal-showing');
var currentState = this;
photo.toggleProperty('fidialogShowing'))
console.log('opendialog');
raw({
url: 'http://example.co.in/api/photo/'+photo.get('like_pk')+'/likes/',
type: 'GET',
}).then(function(result){
currentState.set('model.feed.liker',result)
});
},
bookmarked:function(liker){
liker.set('is_bookmarked',true)
},
}
feed.hbs
<p {{action "fiModal" photo }}>
{{photo.0.numlikes}}
</p>
{{#if photo.fidialogShowing}}
{{#modal-dialog translucentOverlay=true close = (action "fiDialogClose" photo)}}
{{#each model.feed.liker as |liker}}
<div class = "col-sm-6">
{{#if liker.is_bookmarked}}
<a href {{action "unbookmarked" liker}}>
<img class="foll" src = "images/button-bookmark-secondary-state-dark-b-g.png">
</a>
{{else}}
<a href {{action "bookmarked" liker}}>
<img class="foll" src = "images/button-bookmark.png">
</a>
{{/if}}
</div>
{{/each}}
Now the problem is that when action inside the dialog box is fired it throws an error:
fiver.set is not function
I think that the problem is occurring because I am not saving the result in the model store. How should I do it so the action inside the dialog box also works?
You can just encapsulate the results from your server into Ember.Object
Ember.Object.create(json)
For exemple replace your line
currentState.set('model.feed.liker',result)
by
currentState.set('model.feed.liker', result.map(function(item) {
return Ember.Object.create(item);
})
that way each elements inside your model.feed.liker should have a method 'set' available.
I want to add tooltips onto a button in a component that can appear based on a set of results back from the server. (i.e. action buttons for delete, edit etc.)
I have created a “search” component that is rendering into the application and when a search button is clicked the server may return a number of rows into that same search component template.
so for example:
My-app/pods/factual-data/template.hbs
Contains:
…
{{#if results}}
<div class="row">
<div class="col-sm-3"><b>Factual ID</b></div>
<div class="col-sm-2"><b>Name</b></div>
<div class="col-sm-2"><b>Town</b></div>
<div class="col-sm-2"><b>Post Code</b></div>
<div class="col-sm-2"><b>Actions</b></div>
</div>
{{/if}}
{{#each result in results}}
<div class="row">
<div class="col-sm-3">{{result.factual_id}}</div>
<div class="col-sm-2">{{result.name}}</div>
<div class="col-sm-2">{{result.locality}}</div>
<div class="col-sm-2">{{result.postcode}}</div>
<div class="col-sm-2">
<button {{action "clearFromFactual" result.factual_id}} class="btn btn-danger btn-cons tip" type="button" data-toggle="tooltip" class="btn btn-white tip" type="button" data-original-title="Empty this Row<br> on Factual" ><i class="fa fa-check"></i></button>
</div>
</div>
{{/each}}
…
However I cannot get the tooltip code to function, due to an element insert detection/timing issue..
In the component
My-app/pods/factual-data/component.js
Contains:
...
didInsertElement : function(){
console.log("COMPONENT: didInsertElement");
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
this.enableToolTips();
},enableToolTips: function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
}
...
However it seems didInsertElement is only run when the component is first rendered, is there a different function that is called everytime something in the DOM is changed within a component?
I did try to use observes: i.e.
…
enableToolTips: function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
}.observes('results')
…
Which does trigger when the results variable is changed however it is still triggering before the content is actually rendered. I am assuming this because is I manually run in the console Ember.$('.tip').tooltip() (after the button is displayed) then the tooltips work ok.
Any pointers on this issue?
Try
enableToolTips: function() {
Ember.run.scheduleOnce('afterRender', this, function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
});
}.observes('results')
Checking Ember.Component API there are two hooks that can do that
willClearRender : When component html is about to change.
willInsertElement : When old html is cleared and new one is going to be placed.
But you need to have a look on scheduleOnce.
Its worth noting that didInsertElement runs every time. But when it runs view was not updated. To solve that you need to run your code inside a Run Loop like this
didInsertElement : function(){
var self = this;
Ember.run.scheduleOnce('afterRender', this, function(){
//run tool tip here
self.$().find(".tip").tooltip({
});
});
}
I am trying to display the details of items in a list. This should be done by lazy loading the template (DOM for the details), because the template is very large and i've got many items in the list so a ng-show with ng-include is not working, since it is compiled into the DOM and makes the performance very bad.
After experimenting I figured out a solution, only working with a inline template. I am using a click handler to render the HTML with the detail-view directive to the DOM.
HTML
<div ng-controller="Ctrl">
{{item.name}} <button show-on-click item="item">Show Details</button>
<div class="detailView"></div>
<div ng-include="'include.html'"></div>
</div>
<!-- detailView Template -->
<script type="text/ng-template" id="detailView.html">
<p>With external template: <span>{{details.description}}</span></p>
</script>
Show On Click Directive
myApp.directive("showOnClick", ['$compile', '$parse', function($compile, $parse) {
return {
restrict: 'A',
scope: {
item: "=item"
},
link: function (scope, element, attrs) {
// Bind the click handler
element.bind('click', function() {
// Parse the item
var item = $parse(attrs.item)(scope);
// Find the element to include the details
var next = $(element).next('div.detailView');
// Include and Compile new html with directiv
next.replaceWith($compile('<detail-view details="item"></detail-view>')(scope));
});
}
};
}]);
Detail View Directive:
myApp.directive("detailView", ['$parse', '$templateCache', '$http', function($parse, $templateCache, $http) {
return {
restrict: 'E',
replace: true,
templateUrl: 'detailView.html', // this is not working
// template: "<div>With template in directive: <span>{{details.description}}</span></div>", // uncomment this line to make it work
link: function (scope, element, attrs) {
var item = $parse(attrs.details)(scope);
scope.$apply(function() {
scope.details = item.details;
});
}
};
}]);
Here is the full example on
Plunker
Is there a way to improve my solution, or what am I missing to load the external template?
Thanks beforehand!
You can also look at ng-if directive in Angular version 1.1.5 . ng-if would only render the html if condition is true. So this becomes
<div ng-controller="Ctrl">
{{item.name}} <button ng-if="showDetails" item="item" ng-click='showDetails=true'>Show Details</button>
<div class="detailView"></div>
<div ng-include="'include.html'"></div>
</div>
By just using ng-include:
<div ng-controller="Ctrl" ng-init="detailsViewTemplateSource='';">
{{item.name}}
<button ng-click="detailsViewTemplateSource = 'detailView.html'">
Show Details
</button>
<div ng-include="detailsViewTemplateSource"></div>
</div>
<!-- detailView Template -->
<script type="text/ng-template" id="detailView.html">
<p>With external template: <span>{{details.description}}</span></p>
</script>