Creating Dynamic Charts with Chart.JS – Part One: Palindromic Sentences

Hello, and welcome to Another Salesforce Blog!  Here I will be posting solutions to problems that I couldn’t find an answer to in hopes of helping those who find themselves stuck when using the Salesforce platform.

User Story

This is a pretty basic Intro to JavaScript problem (and common interview and homework problem!) that we will be solving via Salesforce. We want to input a sentence or phrase and determine whether or not what we have entered is a palindrome. For kicks and giggles, we also want to count the number of each character present in our input.

Background

We will be building a Lightning Web Component to solve this problem. Although this is a simple example, this is an advanced topic, and will require some advanced setup and knowledge on the part of the user.

Solution

Step One – Create a Sentence Object.

First, we need to create a Sentence object. We want to store the sentence as a text field, a Boolean that shows whether or not the sentence is a palindrome, and the characters that will be used in our sentence. Let’s also add fields for the numerical characters 0-9. Each character will be a number field with no trailing zeroes. The fastest way to do this will be through a spreadsheet.

Create objects from spreadsheet in Salesforce.

Step Two – Create a Lightning Web Component.

Next, we need to create a Lightning Web Component. This will be our parent component, which will host our dynamic charts, which we will create in part two. We’ll call it palindromeChecker.

If you haven’t ever set up Visual Studio Code for Salesforce development, check out this Trailhead for more information.

Once our LWC is set up, let’s also create a Lightning App using the Lightning App Builder. First, we’ll need to modify our palindromeChecker.js-meta.xml file to expose our LWC to the App Builder. Replace the code in your .js-meta.xml file with the following:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="palindromicSentences">
    <apiVersion>50.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

Next, go to your Lightning App Builder and create a new app. We’ll call this Palindrome_Checker, and select a single region. Drag your palindromeChecker component into the region, save, and activate. We want to do this so we can check our progress as we go.

Step Three – Build HTML File.

Let’s get started on our HTML file for our parent component, palindromeChecker.html.

For now, we’re going to create an input field and button that will take up the first third of our screen.

<template>
    <lightning-card title="Palindrome Checker">
        <div class="slds-m-left_medium">
            <div class="slds-grid slds-gutters">
                <div class="slds-col slds-size_1-of-3">
                    <lightning-input type="text" label="Enter a sentence to check:" placeholder="type here..." required></lightning-input>
                    <lightning-button variant="brand" label="Check" title="Check Sentence" onclick={updateSentence} class="slds-m-left_x-small"></lightning-button>
                    <template if:true={sentenceLoaded}>
                        <div class="slds-col">
                            <lightning-card title="Is my sentence a palindrome?">
                                <div class="slds-m-left_medium">
                                    {isPalindrome}
                                </div>
                            </lightning-card>
                        </div>
                    </template>
                </div>
            </div>
        </div>
    </lightning-card>
</template>

We’ll write the updateSentence function shortly, and we will fill out the rest of our screen in the next tutorial.

Right now, our Lightning App should look something like this:

Palindrome Checker Lightning App, Step 3
References:

Lightning-Input
Lightning-Button
Grids in LWC
Margins in LWC

Step Four – Build JavaScript File.

Next, let’s start on our JavaScript file.

First, we’re going to want to import the track and api decorators into our file and define a variable for our text input, sentenceText. We will also define variables for sentence and error, which we will use later on. We then want to write our updateSentence function so that it pulls the value from the lightning-input block and sends it into our palindromeChecker.

import { LightningElement, track, api } from 'lwc';

export default class PalindromeChecker extends LightningElement {

    @track sentenceId
    @track sentenceText
    @track sentence
    @track sentenceLoaded = false;
    @track isPalindrome;
    @track error

    updateSentence(event) {
        let sent = this.template.querySelector('lightning-input').value;
        this.sentenceText = sent;

        this.palindromeChecker(sent);
    }

    //code continuing below...
}

Next, we’re going to write the function that will check to see if the sentence entered is a palindrome or not. This can be done using some basic array functionality in JavaScript.

    palindromeChecker(sent) {
        //create map to hold sentence + boolean value
        let sentenceMap = {};

        //create map to hold sentence + map of characters
        let characterMap = {};

        //verify that text input is not null
        if(sent.length > 0) {
            //create character map to be used in characterCounter
            var charMap = {};

            //regular expression to remove spaces and punctuation
            var rgx = /[\W_]/g;

            //simplify string by making lowercase and replacing all spaces and punctuation with ''
            var simpleString = sent.toLowerCase().replace(rgx, '');

            //send simpleString to characterCounter
            charMap = this.characterCounter(simpleString);

            //use built in javacript functions to split the string into an array character by character,
            //reverse the order of the array, and join the array back into a string
            var reverseString = simpleString.split('').reverse().join('');

            //check to see if the simple string and the reverse string are exactly equal
            var bool = (reverseString === simpleString);

            //set variable tracking whether or not sentence is a palindrome
            this.isPalindrome = bool ? 'Yes! :)' : 'No. :(';

            //escape single quotes
            var escapeQuotes = sent.replace(/'/g,'\'');

            //remove any extra return characters
            var finalStr = escapeQuotes.replace(/^[\r\n]+|\.|[\r\n]+$/g, '');

                //add new key value pairs to map
                sentenceMap[finalStr] = bool;
                characterMap[finalStr] = charMap;
        }
        else {
            alert('Please enter a sentence or phrase!');
        }

        //JSON.stringify results for processing in apex...

        //imperative apex call...
    }

We will write our Apex class momentarily, but let’s stick with Javascript for now and add a characterCounter function. This particular function will use some map magic to get it done.

    characterCounter(str) {
        //create a map to hold character and count
        var counts ={}

        //create counting variables
        var char, index, len, count;

        //loop through string
        for(index = 0, len = str.length; index<len; ++index) {
            char = str.charAt(index);
            //get count for current character
            //will return undefined if character is not yet known
            count = counts[char];

            //if we have seen char, store that count plus one
            //if we have not seen char, store one
            counts[char] = count ? count + 1 : 1;
        }

        return counts;
    }

That’s it! That’s all we can do in our JavaScript file until we build our Apex class. We can test our functionality with some alert() methods in the meantime.

Step Four – Build Apex Class.

Now, let’s save our sentence to our Sentence__c custom object so we can keep track of it. We are going to use the JSON.stringify utility in Javascript to send our information into our Apex class from our JavaScript, so we will add the following lines to our JavaScript file at the end of the palindromeChecker function:

        //set jsonpalindrome variable to the stringified sentenceMap
        let jsonpalindrome = JSON.stringify(sentenceMap);

        //set jsoncharacters variable to the stringified characterMap
        let jsoncharacters = JSON.stringify(characterMap);

Next, we will write our Apex class:

public with sharing class createSentence {

    @AuraEnabled
    public static List<Sentence__c> insertSentence(String JSONpalindrome, String JSONcharacters){

        //deserialize JSON from palindromicSentences.js into Map<String, Boolean>()
        Map<String, Boolean> sentenceMap = (Map<String, Boolean>) JSON.deserializeStrict(JSONpalindrome, Map<String, Boolean>.class);

        //deserialize JSON from palindromicSentences.js into Map<String, Map<String, String>()
        Map<String, Map<String, String>> characterMap = (Map<String, Map<String, String>>) JSON.deserializeStrict(JSONcharacters, Map<String, Map<String, String>>.class);

        //create storage list to reduce DML operations
        List<Sentence__c> newSentences = new List<Sentence__c>();

        //iterate through sentenceMap via key string
        for(String keySentence : sentenceMap.keySet()) {
            //create new Sentence__c object based on specified key value pair
            Sentence__c newSent = new Sentence__c();

            //assignation of values
            newSent.Sentence__c = keySentence;
            newSent.IsPalindrome__c = Boolean.valueOf(sentenceMap.get(keySentence));
            
            //ternary operator null checks to assign numerical values to letter fields
            newSent.A__c = characterMap.get(keySentence).get('a') != null ? Integer.valueOf(characterMap.get(keySentence).get('a')) : 0;
            newSent.B__c = characterMap.get(keySentence).get('b') != null ? Integer.valueOf(characterMap.get(keySentence).get('b')) : 0;
            newSent.C__c = characterMap.get(keySentence).get('c') != null ? Integer.valueOf(characterMap.get(keySentence).get('c')) : 0;
            newSent.D__c = characterMap.get(keySentence).get('d') != null ? Integer.valueOf(characterMap.get(keySentence).get('d')) : 0;
            newSent.E__c = characterMap.get(keySentence).get('e') != null ? Integer.valueOf(characterMap.get(keySentence).get('e')) : 0;
            newSent.F__c = characterMap.get(keySentence).get('f') != null ? Integer.valueOf(characterMap.get(keySentence).get('f')) : 0;
            newSent.G__c = characterMap.get(keySentence).get('g') != null ? Integer.valueOf(characterMap.get(keySentence).get('g')) : 0;
            newSent.H__c = characterMap.get(keySentence).get('h') != null ? Integer.valueOf(characterMap.get(keySentence).get('h')) : 0;
            newSent.I__c = characterMap.get(keySentence).get('i') != null ? Integer.valueOf(characterMap.get(keySentence).get('i')) : 0;
            newSent.J__c = characterMap.get(keySentence).get('j') != null ? Integer.valueOf(characterMap.get(keySentence).get('j')) : 0;
            newSent.K__c = characterMap.get(keySentence).get('k') != null ? Integer.valueOf(characterMap.get(keySentence).get('k')) : 0;
            newSent.L__c = characterMap.get(keySentence).get('l') != null ? Integer.valueOf(characterMap.get(keySentence).get('l')) : 0;
            newSent.M__c = characterMap.get(keySentence).get('m') != null ? Integer.valueOf(characterMap.get(keySentence).get('m')) : 0;
            newSent.N__c = characterMap.get(keySentence).get('n') != null ? Integer.valueOf(characterMap.get(keySentence).get('n')) : 0;
            newSent.O__c = characterMap.get(keySentence).get('o') != null ? Integer.valueOf(characterMap.get(keySentence).get('o')) : 0;
            newSent.P__c = characterMap.get(keySentence).get('p') != null ? Integer.valueOf(characterMap.get(keySentence).get('p')) : 0;
            newSent.Q__c = characterMap.get(keySentence).get('q') != null ? Integer.valueOf(characterMap.get(keySentence).get('q')) : 0;
            newSent.R__c = characterMap.get(keySentence).get('r') != null ? Integer.valueOf(characterMap.get(keySentence).get('r')) : 0;
            newSent.S__c = characterMap.get(keySentence).get('s') != null ? Integer.valueOf(characterMap.get(keySentence).get('s')) : 0;
            newSent.T__c = characterMap.get(keySentence).get('t') != null ? Integer.valueOf(characterMap.get(keySentence).get('t')) : 0;
            newSent.U__c = characterMap.get(keySentence).get('u') != null ? Integer.valueOf(characterMap.get(keySentence).get('u')) : 0;
            newSent.V__c = characterMap.get(keySentence).get('v') != null ? Integer.valueOf(characterMap.get(keySentence).get('v')) : 0;
            newSent.W__c = characterMap.get(keySentence).get('w') != null ? Integer.valueOf(characterMap.get(keySentence).get('w')) : 0;
            newSent.X__c = characterMap.get(keySentence).get('x') != null ? Integer.valueOf(characterMap.get(keySentence).get('x')) : 0;
            newSent.Y__c = characterMap.get(keySentence).get('y') != null ? Integer.valueOf(characterMap.get(keySentence).get('y')) : 0;
            newSent.Z__c = characterMap.get(keySentence).get('z') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X0__c = characterMap.get(keySentence).get('0') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X1__c = characterMap.get(keySentence).get('1') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X2__c = characterMap.get(keySentence).get('2') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X3__c = characterMap.get(keySentence).get('3') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X4__c = characterMap.get(keySentence).get('4') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X5__c = characterMap.get(keySentence).get('5') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X6__c = characterMap.get(keySentence).get('6') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X7__c = characterMap.get(keySentence).get('7') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X8__c = characterMap.get(keySentence).get('8') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            newSent.X9__c = characterMap.get(keySentence).get('9') != null ? Integer.valueOf(characterMap.get(keySentence).get('z')) : 0;
            
            //add new Sentence__c object to storage list
            newSentences.add(newSent);
        }

        //insert storage list newSentences using Database.SaveResult so that
        //the Ids of the new objects can be pulled and sent back to JS file
        Database.SaveResult[] srList = Database.insert(newSentences, false);

        //create storage List<Id> new Ids to store the Ids of new Sentence__c objects
        List<Id> newIds = new List<Id>();

        //iterate over Database.SaveResult list
        for(Database.SaveResult sr : srList) {
            if(sr.isSuccess()) {
                newIds.add(sr.getId());
            }
        }

        //query for the new Sentence__c objects to return back to JS file
        List<Sentence__c> returnSentences = [SELECT Id, Name, Sentence__c, IsPalindrome__c,
                                            A__c, B__c, C__c, D__c, E__c, F__c, G__c, H__c,
                                            I__c, J__c, K__c, L__c, M__c, N__c, O__c, P__c,
                                            Q__c, R__c, S__c, T__c, U__c, V__c, W__c, X__c,
                                            Y__c, Z__c, x0__c, x1__c, x2__c, x3__c, x4__c,
                                            x5__c, x6__c, x7__c, x8__c, x9__c
                                            FROM Sentence__c WHERE Id IN :newIds];

        //return
        return returnSentences;
    }
   
}

Lastly, we import our Apex class by adding it to the top of our JavaScript file:

import insertSentence from '@salesforce/apex/createSentence.insertSentence';

And then add our imperative Apex call into the end of our JavaScript file by adding the following:

        //imperative apex call
        insertSentence({JSONpalindrome : jsonpalindrome, JSONcharacters : jsoncharacters})
            .then(result => {
                //set sentence to the object that is returned
                this.sentence = result[0];    

                //get the Id of the sentence that was created to pass to nested component in part two
                this.sentenceId = result[0].Id;
                this.sentenceLoaded = true;
            })
            .catch(error => {
                this.error = error;
            });

Now, let’s deploy to our org and try it out.

Inserting a test sentence.
Verifying the results of the inserted test sentence.

Now that we have our basic input LWC and underlying structure, we are ready to build our charts. Part two in the series coming soon.

Thanks for reading, let me know if you have any comments or questions!

-Evelyn, Another Salesforce Blog

One thought on “Creating Dynamic Charts with Chart.JS – Part One: Palindromic Sentences

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: