Kanban board Using LWC without Apex
What you will learn in this project
- How to create a lightning Page
- How to create a component
- How to create a local properties
- How to use getter
- How to use @wire
- How to use getObjectInfo, getPicklistValues , updateRecord, and getListUi wire adapters
- How to apply CSS using css file
- How to apply CSS dynamically
- How to achieve components composition
- How to pass data from parent to child
- How to pass data from child to parent
- How Drag and Drop functionality works
- How to update Records without APEX
- How to fetch records without APEX
- How to show toast message
Component Design
Final Output
Video Tutorial
Steps and code
- Go to setup => Quick Find => Lightning App Builder => create New Lightning App Page(Kanban Drag and Drop Using LWC)
- Create Lwc component
DragAndDropLwc
and add the following code to the respective files.
dragAndDropLwc.js
import { LightningElement, wire } from 'lwc';
import { getListUi } from 'lightning/uiListApi';
import { updateRecord } from 'lightning/uiRecordApi';
import { refreshApex } from '@salesforce/apex';
import { getPicklistValues, getObjectInfo } from 'lightning/uiObjectInfoApi';
import OPPORTUNITY_OBJECT from '@salesforce/schema/Opportunity'
import STAGE_FIELD from '@salesforce/schema/Opportunity.StageName'
import ID_FIELD from '@salesforce/schema/Opportunity.Id'
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class DragAndDropLwc extends LightningElement {
records
pickVals
recordId
/*** fetching Opportunity lists ***/
@wire(getListUi, {
objectApiName: OPPORTUNITY_OBJECT,
listViewApiName:'AllOpportunities'
})wiredListView({error, data}){
if(data){
console.log("getListUi", data)
this.records = data.records.records.map(item => {
let field = item.fields
let account = field.Account.value.fields
return { 'Id': field.Id.value, 'Name': field.Name.value, 'AccountId': account.Id.value, 'AccountName': account.Name.value, 'CloseDate': field.CloseDate.value, 'StageName': field.StageName.value, 'Amount': field.Amount.value }
})
}
if(error){
console.error(error)
}
}
/** Fetch metadata abaout the opportunity object**/
@wire(getObjectInfo, {objectApiName:OPPORTUNITY_OBJECT})
objectInfo
/*** fetching Stage Picklist ***/
@wire(getPicklistValues, {
recordTypeId:'$objectInfo.data.defaultRecordTypeId',
fieldApiName:STAGE_FIELD
})stagePicklistValues({ data, error}){
if(data){
console.log("Stage Picklist", data)
this.pickVals = data.values.map(item => item.value)
}
if(error){
console.error(error)
}
}
/****getter to calculate the width dynamically*/
get calcWidth(){
let len = this.pickVals.length +1
return `width: calc(100vw/ ${len})`
}
handleListItemDrag(event){
this.recordId = event.detail
}
handleItemDrop(event){
let stage = event.detail
// this.records = this.records.map(item=>{
// return item.Id === this.recordId ? {...item, StageName:stage}:{...item}
// })
this.updateHandler(stage)
}
updateHandler(stage){
const fields = {};
fields[ID_FIELD.fieldApiName] = this.recordId;
fields[STAGE_FIELD.fieldApiName] = stage;
const recordInput ={fields}
updateRecord(recordInput)
.then(()=>{
console.log("Updated Successfully")
this.showToast()
return refreshApex(this.wiredListView)
}).catch(error=>{
console.error(error)
})
}
showToast(){
this.dispatchEvent(
new ShowToastEvent({
title:'Success',
message:'Stage updated Successfully',
variant:'success'
})
)
}
}
dragAndDropLwc.html
<template>
<div class="card_wrapper">
<template for:each={pickVals} for:item="item">
<div class="stageContainer" key={item} style={calcWidth}>
<h1 class="column_heading">{item}</h1>
<c-drag-and-drop-list records={records} stage={item}
onlistitemdrag={handleListItemDrag}
onitemdrop={handleItemDrop}
></c-drag-and-drop-list>
</div>
</template>
</div>
</template>
create dragAndDropLwc.css file in the component folder and the following code
dragAndDropLwc.css
.card_wrapper{
padding: 0.5rem;
display: flex;
justify-content: space-around;
background: #fff;
}
.column_heading{
padding: 0.5rem 0.25rem;
font-size: 16px;
background: #005fb2;
color: #fff;
margin-bottom: 1rem;
}
.stageContainer{
float:left;
border: 2px solid #d8dde6;
margin: 0.05rem;
border-radius: .25rem;
}
dragAndDropLwc.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
- Create another LWC component
dragAndDropList
and add the following code to the respective files.
dragAndDropList.js
import { LightningElement, api } from 'lwc';
export default class DragAndDropList extends LightningElement {
@api records
@api stage
handleItemDrag(evt){
const event = new CustomEvent('listitemdrag', {
detail: evt.detail
})
this.dispatchEvent(event)
}
handleDragOver(evt){
evt.preventDefault()
}
handleDrop(evt){
const event = new CustomEvent('itemdrop', {
detail: this.stage
})
this.dispatchEvent(event)
}
}
dragAndDropList.html
<template>
<ul class="slds-has-dividers_around-space dropZone"
ondrop={handleDrop}
ondragover={handleDragOver}
style="height:70vh; overflow-y:auto;">
<template for:each={records} for:item="recordItem">
<c-drag-and-drop-card stage={stage} record={recordItem} key={recordItem.Id} onitemdrag={handleItemDrag}></c-drag-and-drop-card>
</template>
</ul>
</template>
dragAndDropList.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
- Create another LWC component
dragAndDropCard
and add the following code to the respective files.
dragAndDropCard.js
import { LightningElement, api } from 'lwc';
import { NavigationMixin } from 'lightning/navigation'
export default class DragAndDropCard extends NavigationMixin(LightningElement) {
@api stage
@api record
get isSameStage(){
return this.stage === this.record.StageName
}
navigateOppHandler(event){
event.preventDefault()
this.navigateHandler(event.target.dataset.id, 'Opportunity')
}
navigateAccHandler(event){
event.preventDefault()
this.navigateHandler(event.target.dataset.id, 'Account')
}
navigateHandler(Id, apiName) {
this[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: Id,
objectApiName: apiName,
actionName: 'view',
},
});
}
itemDragStart(){
const event = new CustomEvent('itemdrag', {
detail: this.record.Id
})
this.dispatchEvent(event)
}
}
dragAndDropCard.html
<template>
<template if:true={isSameStage}>
<li class="slds-item slds-var-m-around_small" draggable="true" ondragstart={itemDragStart}>
<article class="slds-tile slds-tile_board">
<h3 class="slds-truncate" title={record.Name}>
<a href="#" data-id={record.Id} onclick={navigateOppHandler}>
<span class="slds-truncate" data-id={record.Id}>
{record.Name}
</span>
</a>
</h3>
<div class="slds-tile__detail slds-text-body_small">
<p class="slds-text-heading_small">Amount: ${record.Amount}</p>
<p class="slds-truncate" title={record.AccountName}>
<a href="#" data-id={record.AccountId} onclick={navigateAccHandler}>
<span class="slds-truncate" data-id={record.AccountId}>
{record.AccountName}
</span>
</a>
</p>
<p class="slds-truncate" title={record.CloseDate}>Closing {record.CloseDate}</p>
</div>
</article>
</li>
</template>
</template>
create dragAndDropCard.css file in the component folder and the following code
dragAndDropCard.css
.slds-item {
border: 1px solid #d8dde6;
border-radius: 0.25rem;
background-clip: padding-box;
padding: 0.45rem;
margin: 0.25rem;
}
dragAndDropCard.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>48.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
Deployment
Deploy your component to the salesforce org in the following order
- dragAndDropCard
- dragAndDropList
- dragAndDropLwc
Final Output
Now place your component to the page you have created. You will see the following output