Lightning 웹 Components(LWC)에서 요소를 동적으로 추가/제거하는 버튼 설치

무슨 물건이요?


사용자 정의 구성 요소를 만들고 동적 추가(+) 또는 삭제(-) 대상 항목을 위한 단추를 만듭니다.


기본적으로 준비된 구성 요소
사용자 정의 항목의 내용을 동적으로 추가하거나 삭제할 수 없기 때문에 실현할 수 있습니다.

재료 분산


메커니즘으로 JSON 문자열이 변환된 데이터만 사용자 정의 항목(데이터 유형: 긴 텍스트 영역)에 저장합니다.간단하다
따라서 대상의 모든 항목명 취득과 lwc의 특수문법 해설도 함께 넣는다.

(1) 사용자 정의 항목 추가


사용자 정의 구성 요소의 대상에 새 사용자 정의 항목을 추가합니다.
  • 스크래치 조직 열기sfdx force:org:open -u <username/alias>
  • [설정]→[객체 관리자]에서 객체 선택
  • [프로젝트 및 관계]를 통해 새로 작성
  • [새 사용자 정의 프로젝트]에서 긴 텍스트 영역을 선택한 다음
  • [상세 정보 입력]에서 문자 수를 최대 131072로 설정하고 들어갑니다
  • [프로젝트 수준 보안 설정] 기본값은 다음
  • [페이지 레이아웃에 추가]에서 항목을 삭제하는 추가 검사 단추 저장
  • 스크래치 조직의 변경을 미리 pullsfdx force:source:pull -u <username/alias>
  • 7에서 체크 버튼을 제거하여 배치에 JSON 문자열 데이터가 표시되지 않도록 합니다.
    데이터 내용을 확인/표시하려면 검사를 취소하지 않아도 됩니다.

    (2) 권한 집합의 (1) 허용


    AppExchange에서 새로운 프로젝트를 개발할 때 잊어버리기 쉬운 프로젝트입니다.
  • 스크래치 조직 열기sfdx force:org:open -u <username/alias>
  • [설정]→[홈]→[사용 권한 집합]에서 객체 사용 권한 집합
  • 을 선택합니다.
  • [적용]→[객체 설정]
  • 선택
  • 객체를 선택하고 (1)에서 만든 항목에 대한 편집 액세스를 확인하고 저장
  • 스크래치 조직의 변경을 미리 pullsfdx force:source:pull -u <username/alias>
  • (3) 서버측 개발(classes)


    사용자 정의 구성 요소의 뒷면을 만들다.apex로 클래스를 만듭니다.
    디렉토리 구성은 다음과 같습니다.
    root
    └── force-app
        └── main
            └── default
                └── classes
                    ├── [クラス名].cls
    		├── [クラス名].cls-meta.xml
                    ├── [クラス名]_Test.cls
                    └── [クラス名]_Test.cls-meta.xml
    
    예를 들어 프로젝트 덮어쓰기 규칙을 동적으로 추가/삭제하는 구성 요소를 만듭니다.
    Read, Consulting, Partner, Partner Driven 등 4개 객체에 대한 업데이트 가능한 항목을 모두 가져오고 나열하여 덮어쓰기 규칙을 선택/작성할 수 있습니다.
    apex API 버전 v510
    [학급 이름]cls*** ... (1) 의 새 항목을 포함하는 개체 API 참조 이름$$$ ... (1) 의 새 항목 이름
    각각 교체하십시오.
    [학급 이름]cls
    public with sharing class [クラス名] {
        // 「リード」「商談」「取引先」「取引先責任者」のオブジェクトリスト
        static final List<String> OBJECTS_LIST = new List<String>{
            'Lead', 'Opportunity', 'Account', 'Contact'
        };
    
        public static List<***> getRecord(id recId) {
            return [SELECT $$$ FROM *** WHERE id =: recId];
        }    
    
        @AuraEnabled(cacheable=true)
        public static String getRules(id recId) {
            *** record = getRecord(recId)[0];
            if (record.$$$ != null) { return record.$$$; }
            return '[]';
        }
        
        @AuraEnabled
        public static Integer updateRules(id recId, String newRules) {
            try {
    	    if (newRules.length() > 13107) { return 400; }
                List<Object> overwriteInfo = (List<Object>)JSON.deserializeUntyped(newRules);
                for (Object info: overwriteInfo){
                    Map<String, Object> infoMap = (Map<String, Object>)info;
                    List<String> objValue1 = ((String)(infoMap.get('objValue1'))).split(':');
                    List<String> objValue2 = ((String)(infoMap.get('objValue2'))).split(':');
                    if (objValue1.size() != 2 || objValue2.size() != 2 || objValue1 == objValue2) { return 400; }
                    Schema.DisplayType v1 = getTypeByObjField(objValue1[0], objValue1[1]);
                    Schema.DisplayType v2 = getTypeByObjField(objValue2[0], objValue2[1]);
                    if (v1 != v2) { return 400; } 
                }
                List<test_iijima__TEST__c> records = getRecord(recId);
                records[0].test_iijima__Rules__c = newRules;
                update records;
                return 200;
            } catch(DmlException e) {
                return 500;
            }
        }
    
        public static Schema.DisplayType getTypeByObjField(String objName, String fieldName) {
            return Schema.getGlobalDescribe().get(objName).getDescribe().fields.getMap().get(fieldName).getDescribe().getType();
        }
    
        @AuraEnabled(cacheable=true)
        public static Map<String, List<ApiLabelValue>> getObjFeildList(id recId) {
            Map<String, List<ApiLabelValue>> result = new Map<String, List<ApiLabelValue>>();
            for (String objName: OBJECTS_LIST){
                sObject sObj = getSObject(objName);
                result.put(objName, getApiLabelValue(sObj));
            }
            return result;
        }
    
        public static sObject getSObject(String objName) {
            return Schema.getGlobalDescribe().get(objName).newSObject();
        }
    
        public static DescribeSObjectResult getSObjectDescribe(sObject sObj) {
            return sObj.getSObjectType().getDescribe();
        }
    
        public static List<ApiLabelValue> getApiLabelValue(sObject sObj) {
            DescribeSObjectResult sObjectDescribe= getSObjectDescribe(sObj);
            Map<String, SObjectField> sObjectFields = sObjectDescribe.fields.getMap();
            String objName = sObjectDescribe.getName();
            String objLabel = sObjectDescribe.getLabel();
            List<ApiLabelValue> result = new List<ApiLabelValue>{};
            for(SObjectField f : sObjectFields.values()) {
                DescribeFieldResult field  = f.getDescribe();
                if (field.isUpdateable()) {
                    String label = '【' + objLabel + '】' + field.getLabel();
                    String value = objName + ':' + field.getName();
                    ApiLabelValue fields = new ApiLabelValue(label, value);
                    result.add(fields);
                }
            }
            return result;
        }
    
        public class ApiLabelValue{
            @AuraEnabled
            public String label; 
            @AuraEnabled
            public String value;
            public ApiLabelValue(String l, String v) {
                label = l; value = v;
            }
        }
    }
    
    [학급 이름]cls-meta.xml
    [학급 이름]cls-meta.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>51.0</apiVersion>
        <status>Active</status>
    </ApexClass>
    

    해설


    @AuraEnabled
    이 초대장을 지정하면 lwc(전면)에서 사용할 수 있습니다.
    updateRules(id recId, String newRules)
    JSON 문자열 데이터(newRules)를 정면에서 보내는 것은 검증과 업데이트 처리를 실시하는 방법이다.
    발리 2일을 실시했다.
  • 긴 텍스트 영역의 최대 길이 검사
    JSON 문자열 데이터는 긴 텍스트 영역 유형(((1)에 저장되므로 최대 길이인 1317보다 작아야 합니다.newRules.length() > 13107
  • 덮어쓰기 항목의 유형이 동일한지 확인
    이번 예는 프로젝트 간의 덮어쓰기 규칙을 추가/삭제하기 때문에 프로젝트 유형이 같지 않으면 덮어쓸 수 없습니다.
    따라서 getTypeByObjField(String objName, String fieldName)에서 항목 유형을 취득하고 동일 여부를 확인한다.
  • getApiLabelValue(sObject sObj)
    Salesforce에서 제공하는 대상 조회 언어인'soql'은 Select * From [オブジェクトAPI参照名] 등 모든 데이터를 사용할 수 없습니다.
    그래서 모든 데이터가 아니라 모든 프로젝트 이름을 얻으려고 할 때 사용하는 방법을 실현했다.field.isUpdateable() 업데이트 가능한 항목만 가져옵니다.
    반환 값은 (4) lwc에서 사용하는 지정한 형식 (ApiLabelValue) 을 임의로 명명하여 이루어진 것입니다.
    오류 반환값과 검증은 필요한 최소한으로만 이루어집니다. 필요하면 추가/수정하십시오.

    잊지 마라


    스크래치 조직을 미리 변경sfdx force:source:push -u <username/alias>

    (4) 프론트 데스크톱 개발(lwc)


    사용자 정의 구성 요소의 외관을 만듭니다.
    디렉토리 구성은 다음과 같습니다.
    root
    └── force-app
        └── main
            └── default
                └── lwc
    	        └── [カスタムコンポーネント名]
                        ├── [カスタムコンポーネント名].css
    		    ├── [カスタムコンポーネント名].html
    		    ├── [カスタムコンポーネント名].js
                        └── [カスタムコンポーネント名].js-meta.xml
    
    버튼의 수량은 1~50 이내로 제한됩니다.(js 참조)
    lwc API 버전 v51.0
    [구성 요소 이름 사용자 정의]css
    [구성 요소 이름 사용자 정의]css
    .component {
        background-color:white;
        text-align: center;
        padding: 20px 0;
        border-radius: 5px;
    }
    
    .left {
        text-align: left;
        margin-left:10px;
    }
    
    .title {
        margin:10px 0;
        padding-left:10px;
        text-align: left;
        font-size: 15px;
        color: #222222;
    }
    
    .error {
        color: red;
    }
    
    .select {
        text-align: left;
        display: inline-block;
        width: 40%;
    }
    
    .arrow {
        display: inline-block;
        margin: 0 1%;
    }
    
    .button {
        margin-left: 1%;
    }
    
    [구성 요소 이름 사용자 정의]html
    [구성 요소 이름 사용자 정의]html
    <template>
        <div class="component">
            <p class="title">上書きルールの設定</p>
            <template if:true={hasError}>
                <p class="error">{errorMsg}</p>
            </template>
            <div class="left">
                <template iterator:it={rules}>
                    <lightning-combobox
                        key={it.value.objValue1}
                        class="select"
                        label=""
                        value={it.value.objValue1}
                        placeholder="選択してください"
                        options={objFeildList}
                        onchange={changeObjFeildList1}
                        data-index={it.index} >
                    </lightning-combobox>
                    <lightning-icon
                        key={it.value.objValue1}
                        class="arrow"
                        icon-name="utility:forward" >
                    </lightning-icon>
                    <lightning-combobox
                        key={it.value.objValue1}
                        class="select"
                        label=""
                        value={it.value.objValue2}
                        placeholder="選択してください"
                        options={objFeildList}
                        onchange={changeObjFeildList2}
                        data-index={it.index} >
                    </lightning-combobox>
                    <template if:false={isDataSizeMin}>
                        <lightning-button-icon
                            key={it.value.objValue1}
                            variant="bare"
                            data-index={it.index}
                            icon-name="utility:ban"
                            onclick={clickDelete}
                            alternative-text="delete"
                            class="button" >
                        </lightning-button-icon>
                    </template>
                    <template if:false={isDataSizeMax}>
                        <template if:true={it.last}>
                            <lightning-button-icon
                                key={it.value.objValue1}
                                variant="bare"
                                data-index={it.index}
                                icon-name="utility:new"
                                onclick={clickAdd}
                                alternative-text="add"
                                class="button" >
                            </lightning-button-icon>
                        </template>
                    </template>
                    <br key={it.value.objValue1}>
                </template>
            </div>
            <br>
            <lightning-button
                variant="brand"
                label="保存"
                onclick={clickSave}
                disabled={noEdited} >
            </lightning-button>
        </div>
    </template>
    
    [구성 요소 이름 사용자 정의]js
    [구성 요소 이름 사용자 정의]js
    import {LightningElement, api, wire, track} from 'lwc';
    //[クラス名]は(3)で作成したapexクラス名です
    import getObjFeildList from '@salesforce/apex/[クラス名].getObjFeildList';
    import getRules from '@salesforce/apex/[クラス名].getRules';
    import updateRules from '@salesforce/apex/[クラス名].updateRules';
    
    export default class CustomComponent extends LightningElement {
        @api recordId;
        @track rules;
        @track objFeildList;
    
        @wire(getObjFeildList)
        wiredObjFeildList({ error, data }) {
            if (data) {
                this.objFeildList = [
                    {"objValue1":"", "objValue2":"","label":"選択してください"},
                    ...data.Lead,
                    ...data.Opportunity,
                    ...data.Account,
                    ...data.Contact,
                ];
            } else if (error) {
                console.log(JSON.stringify(error, null, '\t'));
            }
        }
    
        @wire(getRules, {recId: '$recordId'})
        wiredRules({ error, data }) {
            if (data) {
                var savedRules = JSON.parse(data);
                if (savedRules.length != 0) {
                    this.rules = savedRules;
                } else {
                    this.rules = [{"objValue1":"", "objValue2":""}];
                }
            } else if (error) {
                console.log(JSON.stringify(error, null, '\t'));
            }
        }
    
        @track noEdited = true;
        @track hasError = false;
        @track errorMsg = "";
    
        get isDataSizeMin() {
            return this.rules.length <= 1 ? true : false;
        }
    
        get isDataSizeMax() {
            return this.rules.length > 50 ? true : false;
        }
    
        changeObjFeildList1(e) {
            this.rules[+e.target.dataset.index].objValue1 = e.detail.value;
            this.noEdited = false;
        }
    
        changeObjFeildList2(e) {
            this.rules[+e.target.dataset.index].objValue2 = e.detail.value;
            this.noEdited = false;
        }
    
        clickAdd() {
            this.rules.push({"objValue1":"", "objValue2":""});
            this.noEdited = false;
        }
    
        clickDelete(e) {
            if (this.rules.length == 1) {
                this.clickAdd();
            } else {
                this.rules.splice(+e.target.dataset.index, 1);
                this.noEdited = false;
            }
        }
    
        clickSave() {
            this.hasError = false;
            let keys = [];
            let savedRules = JSON.parse(JSON.stringify(this.rules));
            if (!savedRules || savedRules.length == 1 && !savedRules[0].objValue1 && !savedRules[0].objValue2) {
                savedRules = [];
            }
            savedRules.forEach( v => {
                if (!v.objValue1 || !v.objValue2) {
                    this.hasError = true;
                    this.errorMsg = "空欄があります。";
                } else if (v.objValue1 == v.objValue2) {
                    this.hasError = true;
                    this.errorMsg = "同じ項目を選択しています。";
                } else {
                    keys.push(v.objValue1 + v.objValue2);
                }
            });
            if (this.hasError) { return; }
            if ((new Set(keys)).size != savedRules.length) {
                this.hasError = true;
                this.errorMsg = "ルールが重複しています。";
                return;
            }
            savedRules = JSON.stringify(savedRules);
            if (savedRules.length > 131072) {
                this.hasError = true;
                this.errorMsg = "ルールをこれ以上追加できません。";
                return; 
            }
            updateRules({ recId: this.recordId, newRules: savedRules })
            .then(result => {
                if (result == 200) {
                    location.reload();
                } else if (result == 400) {
                    this.hasError = true;
                    this.errorMsg = "ルールのデータ型が異なります。";
                } else {
                    this.hasError = true;
                    this.errorMsg = "ルールを追加できませんでした。";
                }
            });
        }
    }
    
    [구성 요소 이름 사용자 정의]js-meta.xml
    [구성 요소 이름 사용자 정의]js-meta.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>51.0</apiVersion>
        <isExposed>true</isExposed>
        <targets>
        	<target>lightning__RecordPage</target>
        </targets>
    </LightningComponentBundle>
    

    해설


    HTML 템플릿(if)
    lwc의 HTML 템플릿에서if 문을 처리할 수 있습니다.
    하지만 연산자사용할 수 없습니다.
    이를 위해 js 측에서 진위 값의 속성 값을 되돌려 주려고 합니다.
    예제
    <!-- if true -->
    <template if:true={hasError}>
    ...
    </template>
    <!-- if false -->
    <template if:false={isDataSizeMin}>
    ...
    </template>
    
    HTML 템플릿
    lwc의 HTML 템플릿에서 두 가지 중복 처리를 처리할 수 있습니다.
    데이터 예
    values = [
        { Id: 1, Name: 'a', Title: 'A' },
        { Id: 2, Name: 'b', Title: 'B' },
        { Id: 3, Name: 'c', Title: 'C' },
    ];
    

  • for:eachfor:index="index"를 사용하여 현재 항목의 인덱스에 액세스합니다.
  • 예1
    <template for:each={values} for:item="value">
        <li key={value.Id}>
            {value.Name}, {value.Title}
        </li>
    </template>
    

  • iterator
    forEach보다 사용이 편리한 것은iterator입니다.value, index 이외
  • first ... Boolean 값
  • last ... 이 항목이 목록의 마지막 항목인지 여부를 표시하는 Boolean 값
    네.
    처리의 시작과 마지막에 디스플레이를 바꾸고 싶을 때 연산자를 사용할 수 없는if문장과 함께 사용할 수 있습니다.
    이번 구현 방식은 요소 추가/삭제 버튼을 통해 마지막 요소인'+'버튼만 설정하거나 요소가 하나일 때'-'버튼의 표시firstlast를 없애는 것이 중요하다.
  • 예2
    <template iterator:it={values}>
        <li key={it.value.Id}>
            <div if:true={it.first} class="list-first"></div>
            {it.value.Name}, {it.value.Title}
            <div if:true={it.last} class="list-last"></div>
        </li>
    </template>
    
    목록의 모든 항목은 key을 포함해야 합니다.key는 문자열이나 숫자여야 하지만 index는 키 값으로 사용할 수 없습니다.
    lwc 라이브러리 (@api, @track, @wire)
    @api
    공통 속성.화면의 변경을 감지하여 처리하다.
    클래스에서 선언@api recordId은 현재 레코드 ID를 자동으로 가져옵니다.
    @track
    이것은 개인 속성이다.화면의 변경을 감지하여 처리하다.
    @wire
    자바스크립트의 변수와 Apex 방법을 연결합니다.매개 변수는 대상을 통해 전달된다.
    예제
    import {LightningElement, api, wire, track} from 'lwc';
    import [メソッド名] from '@salesforce/apex/[クラス名].[メソッド名]';
    export default class [クラス名] extends LightningElement {
        @api recordId;
        @track data;
        @wire([メソッド名], {recId: '$recordId'})
        wiredRules({ error, data }) {
            if (data) {
    	    this.data = data;
            } else if (error) {
                console.log(JSON.stringify(error, null, '\t'));
            }
        }
        ...
    }
    
    필요한 최소한의 검증만 실시했기 때문에 필요하면 추가/수정해 주십시오.

    잊지 마라


    스크래치 조직을 미리 변경sfdx force:source:push -u <username/alias>

    (5) 레이아웃에 사용자 정의 구성 요소 추가


    (3) 스크래치 조직에 (4)를 넣으면 페이지 편집에서 사용자 정의 구성 요소를 추가할 수 있습니다.
  • 스크래치 조직 열기sfdx force:org:open -u <username/alias>
  • 기어 마커 설정에 대한 [편집 페이지]
  • 를 객체 세부 정보 페이지에서 엽니다.
  • 구성 요소 일람[사용자 정의]에서 제작된 구성 요소가 있기 때문에 원하는 위치로 드래그
  • [저장] 및 [유효성]
  • 스크래치 조직의 변경을 미리 pullsfdx force:source:pull -u <username/alias>
  • 완성!


    기본적으로 준비된 구성 요소 종류는 다양하지만 없는 게 없으니 직접 해보세요.
    alesforce가 독자적으로 제공하는 특수 문법을 익히면 할 수 없는 것이 없다.
    그리고 AppExchange 개발이 할 수 있는 일의 폭을 늘렸어요!

    좋은 웹페이지 즐겨찾기