What is the best way to show a loading indicator while retrieving data from Apex in a Lightning Web Component?
I have this approach:
import { LightningElement, api } from "lwc";
import shouldShowCard from "#salesforce/apex/ApexClass.shouldShowCard";
/**
* Card component that is conditionally shown based on Apex.
*/
export default class ConditionalCard extends LightningElement {
#api recordId;
#api isDone = false;
#api shouldShow = false;
connectedCallback() {
shouldShowCard({ id: this.recordId })
.then(result => {
this.shouldShow = result;
})
.finally(() => {
this.isDone = true;
});
}
}
And this HTML
<template>
<template if:false={isDone}>
<div>Loading...</div>
</template>
<template if:true={shouldShow>
<div>Card</div>
</template>
</template>
Now, this works but I'm using the LWC ESLint rules, and when I do this, I get an error/warning "no-api-reassignment" because I'm assigning the api properties in my connectedCallback.
https://github.com/salesforce/eslint-plugin-lwc/blob/master/docs/rules/no-api-reassignments.md
Which seems reasonable, though it very similar to the pattern that the Salesforce Lightning Spinner shows.
https://developer.salesforce.com/docs/component-library/bundle/lightning-spinner/documentation
So I'm just looking for advice on the best way to handle this or if I should just disable the ESLint rule. Other things to consider are how to test this stuff, the reactivity with the API decorator has made things pretty easy on my end but I don't want to continue if I'm not using the best approach.
You don't need #api if these parameters are internal state, if you don't plan to set them from parent component or expose them to System Administrator so he can configure the component in Lightning App Builder for example. You should be fine with just #track - or even no annotation at all. For simple variables you don't need #track since Spring'20 (release notes); you might still need it if your variable is array or object.
This should silence ESLint nicely.
I do it bit differently, personal preference back from Visualforce status and rendered days I guess.
<template>
<template if:true={loaded}>
<p>Content goes here</p>
</template>
<template if:false={loaded}>
<lightning-spinner variant="brand" alternative-text="Loading"></lightning-spinner>
</template>
</template>
import { LightningElement, api, wire, track } from 'lwc';
import someMethod from '#salesforce/apex/SomeClass.someMethod';
export default class StackExample extends LightningElement {
#api recordId;
#track data;
loaded = false;
#wire(someMethod, { i: '$recordId' }) wiredResponse({ error, data }) {
if (data) {
this.data = data;
// some post-processing here
} else if (error) {
// show toast?
}
if(data || error){
this.loaded = true;
}
}
}
Remember that some tags like <lightning-datatable> have internal spinner. Search the documentation for isLoading. So you could even not need the ifs in the html.
Related
I'm new to Salesforce and I'm stuck with showing an imaged added by ContentReference, when adding the image in the Experience Builder it returns a Content Key like this "MCTYRWQGOBCVHMHHLCSYZ2PWXQVQ", but how can I use it to show the selected image in the builder and in the web page I'm building? I tried this solution (https://salesforce.stackexchange.com/questions/333877/spring21-use-cms-content-in-lwc) and adapted it but it throws me the following error :
app:9 [webruntime] router level error
error: Proxy {}
wcstack:
<webruntime-app>
<webruntime-router-container>
<webruntimedesign-component-wrapper>
<webruntimedesign-design-component>
<webruntimedesign-component-wrapper>
<webruntimedesign-design-component>
<c-my-first-l-w-c>
<lightning-layout-item>
Not sure what is happening or what shoul I do, again I'm very new to salesforce. This is my code:
HTML:
<template>
<p>this is the leadlist {contentId}</p>
<img src={contentId} data-contentkey={contentId} class="image"></img>
<lightning-button variant="brand" label={bntLabel} title="Primary action" onclick={handleClick} class="slds-m-left_x-small"></lightning-button>
</template>
JS:
import getManagedContentByContentKeys from '#salesforce/apex/leadList.getManagedContentByContentKeys';
import { LightningElement, api, wire } from 'lwc';
export default class LeadList extends LightningElement {
#api bntLabel;
#api contentId;
handleClick = () => {
console.log("You clicked me!")
console.log('contentId', this.contentId)
}
#wire(getManagedContentByContentKeys, { managedContentIds: this.contentId})
managedContent({ error, data }) {
console.log('it entered the function:');
if (data) {
console.log('data:');
console.log({data});
// Assign data to a variable which you can use in your HTML.
} else if (error) {
console.log('error:', error);
// Handle the error.
}
}
}
Metadata:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
<targetConfigs>
<targetConfig targets="lightningCommunity__Default">
<property name="bntLabel" type="String" default="click"></property>
<property type="ContentReference" name="contentId" label="Content ID"></property>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Apex class:
public with sharing class leadList {
public leadList() {
}
#AuraEnabled(cacheable=true)
public static String getManagedContentByContentKeys(String communityId, String[] managedContentIds, Integer pageParam, Integer pageSize, String language, String managedContentType, Boolean showAbsoluteUrl){
return 'hola';//ConnectApi.ManagedContent.getManagedContentByContentKeys(communityId, managedContentIds, pageParam, pageSize, language, managedContentType, showAbsoluteUrl);
}
}
It looks like there are a few minor bugs in the code and that is the reason you aren't having much luck.
Firstly, the #wire method in the LWC requires an array of managedContentIds, not just a single one. The syntax for the dynamic variable is also incorrect (LWC Documentation: Wire Adapters).
import getManagedContentByContentKeys from '#salesforce/apex/leadList.getManagedContentByContentKeys';
import { LightningElement, api, wire } from 'lwc';
export default class LeadList extends LightningElement {
#api bntLabel;
#api contentId;
// Added the contentId to an array of contentIds
get managedContentIds() {
return [this.contentId]
}
handleClick = () => {
console.log("You clicked me!")
console.log('contentId', this.contentId)
}
// Fix the syntax for dynamic props for `#wire` methods.
#wire(getManagedContentByContentKeys, { managedContentIds: '$managedContentIds'})
managedContent({ error, data }) {
if (error) {
console.error('error:', error);
} else if (data) {
console.log('data: ', data);
// destructure the properties we want from the response object
const {
managedContentId,
contentNodes: { source, altText, title },
} = data
// Data here should be set to a property for the html to render
this.image = {
url: source ? source.unauthenticatedUrl : '',
alt: altText ? altText.value : '',
title: title ? title.value : '',
}
}
}
}
The apex method also returns a test string (I assume this was a mistake returning "hola"). You can also set the extra properties to null/true if you don't intend on passing them.
public with sharing class leadList {
public leadList() { }
#AuraEnabled(Cacheable=true)
public static String getManagedContentByContentKeys(String communityId, String[] managedContentIds){
return ConnectApi.ManagedContent.getManagedContentByContentKeys(
communityId,
managedContentIds,
null,
null,
null,
null,
true
);
}
}
Lastly, the html should render the this.image we set above in the javascript. Instead of assigning contentId to the <img src={contentId}/>, it should use the this.image reference, <img src={image.url} alt={image.alt}/>
<template>
<p>this is the leadlist {contentId}</p>
<img src={image.url} alt={image.alt} class="image"></img>
<lightning-button variant="brand" label={bntLabel} title="Primary action" onclick={handleClick} class="slds-m-left_x-small"></lightning-button>
</template>
This question should probably have been posted on salesforce.stackexchange.com.
I have a custom LWC widget in Account record page, when page loaded my widget, I need to know the account's ID or record id(e.g. "0015g00000Bkgg6AAB"),
my testing code below:
import { LightningElement, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
const fields = [NAME_FIELD, REVENUE_FIELD, BillingAddress_FIELD];
export default class AccountCreator extends LightningElement {
#wire(getRecord, { recordId: '$recordId', fields })
account;
get name() {
return getFieldValue(this.account.data,NAME_FIELD);
}
get revenue() {
return getFieldValue(this.account.data, REVENUE_FIELD);
}
get address() {
return getFieldValue(this.account.data, BillingAddress_FIELD);
}
}
Add these two lines in your class
import { LightningElement, wire, api } from 'lwc'; // 1 - add "api"
export default class AccountCreator extends LightningElement {
#api recordId; // 2 - add variable to magically store the current record's id
}
There are 2 special variables for learning current record's id and current sObject type (if you want to build component that behaves differently when dropped on different pages). Some more info here: https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_record_context
Your code looks like you copied from an example, your #wire already listens to changes of the recordId - you just didn't have this variable defined.
I try to get a value from a database. It is sensor data, which i want to save from an odroid on the database.
The problem is, I cannot get the value into my ionic 3 app.
The PHP file should work, when i open the file, i get the right number in the browser. for testing i also took just a .txt-file with the number 9 inside. but it still doesn´t work. It still stays empty. I have global variables into the provider, which i want to hold up-to-date.
Here is my code:
global.ts (provider)
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable()
export class GlobalProvider {
dif1:number;
constructor(public http: HttpClient){}
load() {
this.http.get("http://192.168.131.221/test/ultras.1.txt")
.subscribe((data : any) => {
this.dif1 = parseInt(JSON.stringify(data));
})
}
}
data.html (call of the provider)
<button ion-button (click)="global.load()">push</button>
<div class="llDataContainer">
<div class="llDataBox llDataBox-2"></div>
<div class="llDataBox llDataBox-2"></div>
<div class="llDataBox llDataBox-2">ultra1:</div>
<!--distance value (ultra1)-->
<div class="llDataBox llDataBox-2">{{ global.dif1 }} cm</div>
If i push the button, still it stays empty.
the good thing is that i get a response now. the bad thing: the response is NAN...
I tried this:
load() {
this.http.get('url').
toPromise().then(result => {
this.dif1 = parseInt(JSON.stringify(result));
})
}
I have been trying for a while to learn how to build mobile apps with Javascript, and honestly I have no idea how anyone is able to do anything. Everything is broken. Every tutorial I've tried has failed to work for some bizarre reason. I am at my wits end.
I've finally decided to try and be even simpler, and just do the most basic tutorial I can find. What could go wrong. Well, it only took 3 pages of almost no code to completely stop working. I've done this, and I cannot insert anything to my db. My app fetches no data. When trying to add a new task, it gets added then disappears almost immediately, with a message stating insert failed: Method '/tasks/insert' not found (not even an error with some traceback).
The code really couldn't be simpler:
// imports/api/tasks.js
import { Mongo } from 'meteor/mongo';
export const Tasks = new Mongo.Collection('tasks');
// imports/ui/App.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data'
import { Tasks } from '../api/tasks.js';
import Task from './Task.js';
// App component - represents the whole app
class App extends Component {
handleSubmit(event) {
event.preventDefault();
// find the text field via the react ref
const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
Tasks.insert({ text, createdAt: new Date() });
// Clear form
ReactDOM.findDOMNode(this.refs.textInput).value = '';
}
renderTasks() {
return this.props.tasks.map((task) => (
<Task key={task._id} task={task} />
));
}
render() {
return (
<div className="container">
<header>
<h1>Todo List</h1>
<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
<input
type="text"
ref="textInput"
placeholder="Type to add new tasks"
/>
</form>
</header>
<ul>
{this.renderTasks()}
</ul>
</div>
);
}
};
export default withTracker(() => {
return {
tasks: Tasks.find({}).fetch(),
};
})(App);
What is wrong? What am I missing?
The tutorial is indeed out of date and should be updated.
Background
In June 2017 there was a big security issue with allow/deny identified and the feature has been blocked since then.
Meteor allowed you to define client collection, that automatically synced with the server when the methods insert, update, remove were called on the client.
In order to control the access permissions, the allow/deny feature was implemented.
Now without allow/deny you will get the insert failed: Method '/tasks/insert' not found when classing SomeCollectionOnClient.insert but since this feature is obsolete (you will even get a big warning when setting it up), you need to create a server side method and call it from the client in order resolve this issue:
On the server create this method and ensure it is in the import chain from server/main.js:
new ValidatedMethod({
name: 'tasks.insert',
validate(args) {
// better use simpl-schema here
if (!args.text || !args.createdAt) {
throw new Meteor.Error('incompleteArgs', 'args are incomplete')
}
},
run (args) {
// check user permissions...
return Tasks.insert({ text, createdAt })
}
})
In your client component you can then call it via:
// find the text field via the react ref
const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
Meteor.call('tasks.insert', {text, createdAt: new Date()}, (err, res) => {
// do something on err / on res
})
Note that this couples your component to the server side method and you may rather try to implement some containers for your pages that handle all the connection / pub-sub / method calling activity wile your components solely render content.
More to read / used in this answer:
https://guide.meteor.com/react.html
https://guide.meteor.com/security.html
https://docs.meteor.com/api/methods.html#Meteor-call
https://guide.meteor.com/methods.html#validated-method
I would like to know how to call a Firebase array using Angular 2. In my example here, I have an array addnote in my Firebase DB. There are two separate iterations of Do the dishes, and I would like to print them out to my HTML's unordered list.
The [] in my private addsnotes throws errors, and I didn't really expect otherwise. In the absence of understanding how to output the array, I am using it to illustrate what I am trying to achieve. I have also marked the relevant area where the call is being made.
My rainbow.component.html
<div><ul>
<li *ngFor="let addsnote of addsnotes">{{addsnote}}</li>
</ul></div>
My firebase schema:
My rainbow.component.ts
export class Rainbow implements OnInit{
private addsnotes: [];
private username: string;
ngOnInit(){
var self = this;
var user = firebase.auth().currentUser;
var getUserInfo = firebase.database().ref('users/' + user.uid);
setTimeout(acquisition, 1000);
function acquisition(){
if (user){
getUserInfo.once('value', function(snapshot){
self.username = snapshot.val().username;
self.addsnotes = snapshot.val().addnote; //incorrect
});
}
}
}
}
If you want the AngularFire2 makes is easy to tune into the power of Observables so you can detect changes on the firebase end and auto-update your user notes. With AngularFire2, your solution would look more like this...
rainbow.component.html
<div>
<ul>
<li *ngFor="let addsnote of addsnotes$ | async">{{ addsnote.$value }}</li>
</ul>
</div>
rainbow.component.ts
import { Injectable } from '#angular/core';
import { AngularFire } from 'angularfire2';
import { Observable } from 'rxjs/Observable';
export class RainbowComponent implements OnInit {
private addsnotes$: Observable<string[]>;
private username$: Observable<string>;
constructor (
private af: AngularFire
) {}
ngOnInit () {
let user = firebase.auth().currentUser,
userNamePath = `users/${user.uid}/username`,
notesPath = `users/${user.uid}/addnote`;
this.username$ = this.af.database.object(userNamePath);
this.addsnotes$ = this.af.database.list(notesPath);
}
}
You will need the async pipe when using Observables in your template HTML. It will auto subscribe to extract the data. The big difference with this and the previous code is that anytime your addsnotes data changes, it will automatically show the changes on the HTML view. If you want to keep it like the previous code where you are limiting it to one call using once('value'), you can add a .take(1) to the end of this.af.database.list(notesPath) to just take the list values one time.
In addition, I would recommend adding a sub field to your notes such as order so that you can sort your list in an order that you want. You can find info on how to sort with AngularFire2 here.
Hope this helps.
If you wanted to stick with Web Firebase API (no angularfire2), to get the addsnotes to work, it might look something like this.
ngOnInit () {
let user = firebase.auth().currentUser;
// Used ES6 arrow function instead, which is built into Typescript
setTimeout(() => {
// Make sure user and user.uid are defined
if (user && user.uid) {
let userRef = firebase.database().ref(`users/${user.uid}`);
userRef.once('value', (snapshot) => {
let userInfo = snapshot.val() || {};
this.username = userInfo['username'];
this.addsnotes = [];
// Traverse each child for 'addnote'
snapshot.child('addnote').forEach((childSnapshot) => {
let addnoteKey = childSnapshot.key,
addnote = childSnapshot.val();
addnote['id'] = addnoteKey; // Saving the reference key if you want reference it later
self.addsnotes.push(addnote);
});
}
}
}, 1000);
}