3

Found this code online as an open source and I've been trying to convert it to a LWC. The code is in HTML, CSS, and JS. Am working on it in visual studio and am using a salesforce extension pack that doesn't accept HTML, it needs tags but I have never used template tags before. It also gives me the error of, meta tag is not allowed I have no clue what this error is. Can anybody help? Error is on line 3

<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />

        <title>Maths Game</title>

        <link rel="stylesheet" href="mathGame.css" />
    </head>

    <body>
        <main>
            <div id="container">
                <p id="message" class="structure-elements"></p>
                <aside id="score" class="structure-elements">Score: <span>00</span></aside>

                <div id="calculation">
                    <section id="question" class="structure-elements"></section>
                    <p id="instruction" class="structure-elements">Click on the correct answer</p>
                    <ul id="choices" class="structure-elements">
                        <li></li>
                        <li></li>
                        <li></li>
                        <li></li>
                    </ul>
                </div>

                <button id="start-reset" class="structure-elements">Start Game</button>

                <aside id="time-remaining" class="structure-elements">Time remaining: <span>60</span> sec</aside>
            </div>

            <div id="game-over" class="structure-elements">
                <p>Game over!</p>
                <p>Your score is <span>00</span>.</p>
            </div>
        </main>

        <script src="mathGame.js"></script>
    </body>
</html>

**This is the Javascript portion of the code

var Counter = {
    PlayingState: null,
    IsStoped: true,
    Score: 0,
    TimeRemaining: 0,
    FirstNumber: 0,
    SecondNumber: 0,
    CorrectAnswer: 0,
    CorrectPosition: 0,
    WrongAnswer: 0,
    AddContentToElement: function(selector, content)
    {
        document.querySelector(selector).innerHTML = content;
    },
    ChangeStyle: function(selector, property, value)
    {
        document.querySelector(selector).setAttribute(property, value);
    },
    Initialize: function(timeRemaining)
    {
        this.TimeRemaining = timeRemaining;
    },
    GenerateRandomNumber: function(multiplier)
    {
        return Math.round( Math.random() * multiplier ) + 1;
    },
    Refresh: function(selector, data)
    {
        document.querySelector(selector).innerText = (data < 10 ? "0" : "") + data;
    },
    LoopThroughElements: function()
    {
        var answers = [this.CorrectAnswer];

        for (var index = 1; index < 5; index++)
        {
            this.ChangeStyle("ul#choices > li:nth-of-type(" + index + ")", "style", "height:auto;");

            if (index !== this.CorrectPosition)
            {
                do
                {
                    this.WrongAnswer = this.GenerateRandomNumber(9) * this.GenerateRandomNumber(9);
                } while ( answers.indexOf(this.WrongAnswer) > -1 );

                this.AddContentToElement( "ul#choices > li:nth-of-type(" + index + ")", this.WrongAnswer );
                answers.push(this.WrongAnswer);
            }
        }
    },
    Launch: function()
    {
        this.IsStoped = false;
        this.Action();
        this.ChangeStyle("aside#time-remaining", "style", "visibility:visible;");
        this.ChangeStyle("#game-over", "style", "display:none;");
        this.ChangeStyle("ul#choices", "style", "pointer-events:initial; opacity:1;");
        this.ChangeStyle("button#start-reset", "style", "visibility:hidden;");
        this.AddContentToElement("button#start-reset", "Reset Game");
        this.Refresh("aside#time-remaining > span", this.TimeRemaining);
        this.GenerateQuestionAndAnswers();
    },
    GenerateQuestionAndAnswers: function()
    {
        this.FirstNumber = this.GenerateRandomNumber(9);
        this.SecondNumber = this.GenerateRandomNumber(9);
        this.CorrectAnswer = this.FirstNumber * this.SecondNumber;
        this.CorrectPosition = this.GenerateRandomNumber(3);
        this.ChangeStyle("section#question", "style", "height:auto;");
        this.AddContentToElement("section#question", this.FirstNumber + "x" + this.SecondNumber);
        this.AddContentToElement( "ul#choices > li:nth-of-type(" + this.CorrectPosition + ")", this.CorrectAnswer );
        this.LoopThroughElements();
    },
    Action: function()
    {
        Counter.PlayingState = setInterval( function()
        {
            Counter.TimeRemaining--;
            
            if (Counter.TimeRemaining <= 50)
            {
                Counter.ChangeStyle("button#start-reset", "style", "visibility:visible;");
            }

            if (Counter.TimeRemaining < 1)
            {
                Counter.Stop();
            }
            else
            {
                Counter.Refresh("aside#time-remaining > span", Counter.TimeRemaining);
            }
        }, 1000 );
    },
    EventListener: function(event)
    {
        if ( Number(event.currentTarget.innerText) === Number(Counter.CorrectAnswer) )
        {
            Counter.Score++;
            Counter.Refresh("aside#score > span", Counter.Score);
            Counter.GenerateQuestionAndAnswers();
            Counter.ChangeStyle("p#message", "style", "visibility:visible; background-color:#23A230;");
            Counter.AddContentToElement("p#message", "Correct");
        }
        else
        {
            if (Counter.Score >= 1)
            {
                Counter.Score -= 0.5;
                Counter.Refresh("aside#score > span", Counter.Score);
            }
            
            Counter.ChangeStyle("p#message", "style", "visibility:visible; background-color:#DE401A;");
            Counter.AddContentToElement("p#message", "Try again");
        }

        setTimeout( function()
        {
            Counter.ChangeStyle("p#message", "style", "visibility:hidden;");
        }, 1000 );
    },
    CheckClickOnRightAnswer: function()
    {
        for (var index = 1; index < 5; index++)
        {
            document.querySelector("ul#choices > li:nth-of-type(" + index + ")").removeEventListener("click", this.EventListener, false);
            document.querySelector("ul#choices > li:nth-of-type(" + index + ")").addEventListener("click", this.EventListener);
        }
    },
    Stop: function()
    {
        this.IsStoped = true;
        clearInterval(this.PlayingState);
        this.ChangeStyle("ul#choices", "style", "pointer-events:none; opacity:0.4;");
        this.ChangeStyle("aside#time-remaining", "style", "visibility:hidden;");
        this.ChangeStyle("div#game-over", "style", "display:block;");
        this.AddContentToElement("button#start-reset", "Start Game");
        this.AddContentToElement( "div#game-over > p:last-of-type > span", (this.Score !== "00" && this.Score < 10 ? "0" : "") + this.Score );
        this.AddContentToElement("aside#score > span", this.Score = "00");
    }
};


/*************************************************************************************************/
/* ************************************** CODE PRINCIPAL *************************************** */
/*************************************************************************************************/
document.addEventListener('DOMContentLoaded', function()
{
    document.getElementById("start-reset").addEventListener("click", function()
    {
        Counter.Initialize(60);
        Counter.IsStoped ? Counter.Launch() : Counter.Stop();
        Counter.CheckClickOnRightAnswer();
    });
});

1 Answers1

0

With LWC you don't write full page apps, no <html>, <head>, <body>. You write small reusable components with <template> tag and they can be dropped on various pages. And most of the time you don't manipulate HTML directly. You set values to JS variables and framework rerenders relevant parts. Makes it simpler to split presentation and logic and makes the logic testable (yes, there can be unit tests for LWC).

These self-paced trainings might be handy: https://trailhead.salesforce.com/content/learn/modules/modern-javascript-development, https://trailhead.salesforce.com/en/content/learn/modules/lightning-web-components-basics

So... If you just want to get it to work in Salesforce you can always make a Visualforce page out of it, that's closest to a full page app. Or upload your stuff as a static resource and then use ligthning:container Aura component to load it up. It will load as iframe but styles won't clash, there's minimum JS knowledge required to port the app, sometimes this is the way. Bit more "pro" would be to try to rewrite it as little as possible. This thing heavily manipulates the raw HTML and that's not exactly LWC way but it's possible with lwc:dom="manual"

If you feel adventurous - rewrite it to LWC. This is far from perfect and not a full rewrite but should give you some ideas.

html

<template>
    <div>
        <lightning-layout multiple-rows="true">
            <lightning-layout-item size="6"><span class={messageStyle}>{message}</span></lightning-layout-item>
            <lightning-layout-item size="6">
                Score: 
                <lightning-formatted-number value={score} minimum-integer-digits="2"></lightning-formatted-number>
            </lightning-layout-item>

            <template if:false={isStopped}>
                <lightning-layout-item size="12"><span class="question">{question}</span></lightning-layout-item>
                <lightning-layout-item size="12">Click on the correct answer</lightning-layout-item>

                <template for:each={answers} for:item="a">
                    <lightning-layout-item key={a.value} size="3">
                        <lightning-button label={a.value} value={a.value} variant="neutral" onclick={handleAnswerClick}></lightning-button>
                    </lightning-layout-item>
                </template>
            </template>

            <lightning-layout-item size="6">
                <template if:true={isButtonVisible}>
                    <lightning-button label={buttonLabel} variant={buttonVariant} onclick={handleButtonClick}></lightning-button>
                </template>
            </lightning-layout-item>
            <lightning-layout-item size="6">
                Time remaining: 
                <lightning-formatted-number value={timeRemaining} minimum-integer-digits="2"></lightning-formatted-number>
            </lightning-layout-item>
        </lightning-layout>
    </div>
</template>

js

import { track, LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent'

export default class Stack63257378 extends LightningElement {
    buttonLabel = 'Start Game';
    buttonVariant = 'success';
    isButtonVisible = true;

    message;
    messageStyle;

    firstNumber;
    secondNumber;
    @track answers = [];

    timeRemaining;
    score;
    isStopped = true;

    handleButtonClick(event){
        this.isStopped ? this.launch() : this.stop();
    }

    launch(){
        this.timeRemaining = 15; // 60
        this.isStopped = this.isButtonVisible = false;
        this.score = 0;
        let interval = setInterval(function (){
            --this.timeRemaining;
            if(this.timeRemaining <= 10){ // 50
                this.isButtonVisible = true;
                this.buttonLabel = 'Stop Game';
                this.buttonVariant = 'destructive';
            }
            if(this.timeRemaining < 1){
                clearInterval(interval);
                this.stop();
            }
        }.bind(this),1000);

        this.generateQuestion();
    }

    handleAnswerClick(event){
        if(this.correctAnswer === event.target.value){
            ++this.score;
            this.generateQuestion();
            this.message = 'Correct';
            this.messageStyle = 'good';
        } else {
            if(this.score >= 1) {
                this.score -= 0.5;
            }
            this.message = 'Try again';
            this.messageStyle = 'bad';
        }
    }

    stop(){
        this.answers = [];
        this.isStopped = true;
        this.buttonLabel = 'Start Game';
        this.buttonVariant = 'success';
        this.message = this.messageStyle = null;
        
        const event = new ShowToastEvent({
            title: 'Game over!',
            message: `Your score is ${this.score}`,
        });
        this.dispatchEvent(event);
    }

    generateQuestion(){
        this.firstNumber = this.getRandomNumber(9);
        this.secondNumber = this.getRandomNumber(9);
        this.correctAnswer = this.firstNumber * this.secondNumber;
        this.answers = [];
        let correctPosition = this.getRandomNumber(3);
        for(let i = 0; i < 4; ++i){
            let obj = {"i" : i, "value" : i === correctPosition ? this.correctAnswer : this.getRandomNumber(9) * this.getRandomNumber(9)};
            this.answers.push(obj);
        }
    }

    getRandomNumber(range){
        return Math.round( Math.random() * range ) + 1
    }

    get question(){
        return this.isStopped ? '' : `${this.firstNumber} x ${this.secondNumber}`;
    }
}

css

.good {
    background-color:#23A230;
}
.bad {
    background-color:#DE401A;
}
.question {
    font-size: 24px;
}
button {
    width: 100%;
}

"meta-xml" (decides where you can embed this component in SF)

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>48.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Math game</masterLabel>
    <description>https://stackoverflow.com/q/63257378/313628</description>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>
eyescream
  • 18,088
  • 2
  • 34
  • 46