21
Lightning コンポーネント開発の勘所 Salesforce World Tour Tokyo 2016 [Session 9-4] 2016年12月14日 株式会社テラスカイ 製品開発部 SuPICE / SkyVisualEditor プロダクトマネージャー 吉田 寛

Lightning コンポーネント開発の勘所 - Amazon S3...Lightning コンポーネント開発の勘所 Salesforce World Tour Tokyo 2016 [Session 9-4] 2016年12月14日 株式会社テラスカイ

  • Upload
    others

  • View
    7

  • Download
    0

Embed Size (px)

Citation preview

Lightning コンポーネント開発の勘所

Salesforce World Tour Tokyo 2016[Session 9-4]

2016年12月14日

株式会社テラスカイ 製品開発部

SuPICE / SkyVisualEditor プロダクトマネージャー

吉田 寛

Copyright © TerraSky Co., Ltd. All Rights Reserved. 2

コンサルティング

クラウド・インテグレーション

クラウドサービス運用支援

Copyright © TerraSky Co., Ltd. All Rights Reserved.

Copyright © TerraSky Co., Ltd. All Rights Reserved. 4

どんな

Lightningコンポーネント

が作れる?

Copyright © TerraSky Co., Ltd. All Rights Reserved. 5

Copyright © TerraSky Co., Ltd. All Rights Reserved. 6

Copyright © TerraSky Co., Ltd. All Rights Reserved. 7

コーディングしてみる?

Copyright © TerraSky Co., Ltd. All Rights Reserved. 8

【AccountSearch.cmp】

<aura:component controller="AccountSearchController" implements="force:appHostable,flexipage:availableForAllPageTypes">

<ltng:require scripts="/resource/TS_AccountSearch/jquery-1.11.3.min.js,

/resource/TS_AccountSearch/lodash.min.js" />

<aura:handler name="init" value="{!this}" action="{!c.doInit}" />

<div class="condition">

<!-- SearchBox Start -->

<div class="section">

<div class="sectionTitile">Search Box</div>

<div class="searchCondition">

<div class="searchItem">

<span class="searchItemLabel"></span>

<input type="text" id="selectInputItem" palaceholder="Account Name"/>

</div>

<div class="searchItem">

<span class="searchItemLabel"></span>

<select id="selectSearchItem">

<option value="">--None--</option>

</select>

</div>

<div class="searchButtonBox">

<input class="searchButton" type="button" value="Search" onclick="{!c.doSearch}"/>

</div>

</div>

</div>

<!-- Results Start -->

<div class="section">

<div class="sectionTitile">Search Results</div>

<div class="searchResult"></div>

</div>

</div>

<!-- detail Start -->

<div class="detail" style="display:none">

<div class="section">

<div id="TS_DetailSection" tabindex="0" class="sectionTitile">Detail</div>

<div class="detailList"></div>

<input type="button" value="Close" onclick="{!c.closeDetail}"/>

</div>

</div>

</aura:component>

【AccountSearchController.js】

({

doInit : function(component, event, helper){

var action = component.get('c.getOptions');

action.setCallback(this,function(response){

var sel = document.getElementById('selectSearchItem');

for(var i =0; i<response.getReturnValue().length; i++){

var val = response.getReturnValue()[i].BillingState;

if(val){

var op = document.createElement('option');

op.setAttribute('value',val);

op.innerHTML = val;

sel.appendChild(op);

}

}

});

$A.enqueueAction(action);

},

doSearch : function(component, event, helper) {

var accName = document.getElementById('selectInputItem').value;

var accState = document.getElementById('selectSearchItem').value;

var action;

if(accName !="" && accState == ""){

action = component.get('c.getAccountName');

}else if (accName == "" && accState != ""){

action = component.get('c.getAccountState');

}else{

action = component.get('c.getAccounts');

}

action.setParams({

'accName':accName,

'accState':accState

});

action.setCallback(this,function(response){

var str = '<% records.forEach(function (r) { %>¥

<div class="wrap">¥

<div class="mapframe">¥

<img class="map" src="/resource/mapicon/mapicon/icon_1r_64.png" address="<%= r.Street %>" />¥

</div>¥

<div id="<%= r.Id %>" class="recordList">¥

<div class="Name"><%= r.Name %></div>¥

<div>BillingAddress <br/>¥

Country : <%= r.Country %><br/>¥

PostalCode : <%= r.PostalCode %><br/>¥

State : <%= r.State %><br/>¥

City : <%= r.City %><br/>¥

Street : <%= r.Street %>¥

</div>¥

<div>Phone : <%= r.Phone %></div>¥

</div>¥

</div>¥

<% }); %>';

helper.setResult(component,str,response.getReturnValue(),event);

});

$A.enqueueAction(action);

},

closeDetail: function(){

// $('.detail').fadeOut('normal');

$('.detail').hide();

$('.condition').fadeIn('normal');

},

navigate : function() {

},

})

【AccountSearchHelper.js】

({

setResult: function(cmp,str,record,event) {

var self = this;

function toArray(fakeArray) {

return Array.prototype.slice.call(fakeArray);

}

$(function () {

var records = Array.apply(null, new Array(record.length)).map(function (n, i) {

var address = encodeURIComponent(record[i].BillingStreet);

return {

Id: record[i].Id,

Name: record[i].Name,

Country: record[i].BillingCountry,

PostalCode: record[i].BillingPostalCode,

State: record[i].BillingState,

City: record[i].BillingCity,

Street: record[i].BillingStreet,

Phone: record[i].Phone,

Address: address

};

});

var template = _.template(str);

document.getElementsByClassName('searchResult')[0].innerHTML = template({records: records});

});

/* 詳細表示用 */

$('.recordList').click(function (ev) {

self.setDetailList(cmp,record,ev);

$('.detail').fadeIn('normal');

$('.condition').hide();

});

/* 地図表示用 */

$('.map').click(function (e) {

var address = encodeURIComponent($(e.target).attr('address'));

var urlEvent = $A.get("e.force:navigateToURL");

urlEvent.setParams({

"url": 'https://www.google.com/maps/place/' + address

});

urlEvent.fire();

});

},

setDetailList: function(cmp,record,ev){

var self = this;

var recordId = ev.currentTarget.id;

var str = '<% records.forEach(function (r) { %>¥

<div class="recordDetail">¥

<div class="Name"><%= r.Name %></div>¥

<div>BillingAddress <br/>¥

Country : <%= r.Country %><br/>¥

PostalCode : <%= r.PostalCode %><br/>¥

State : <%= r.State %><br/>¥

City : <%= r.City %><br/>¥

Street : <%= r.Street %>¥

</div>¥

<div>Phone : <%= r.Phone %></div>¥

<div class="conRelatedList">¥

<table>¥

<thead>¥

<tr>¥

<th>No.</th><th>Contact Name</th>¥

</tr>¥

</thead>¥

<tbody class="conRelatedListBody">¥

</tbody>¥

</table>¥

</div>¥

<div class="oppRelatedList">¥

<table>¥

<thead>¥

<tr>¥

<th>No.</th><th>Opportunity Name</th>¥

</tr>¥

</thead>¥

<tbody class="oppRelatedListBody">¥

</tbody

</table>¥

</div>¥

</div><br/>¥

<% }); %>'

function toArray(fakeArray) {

return Array.prototype.slice.call(fakeArray);

}

$(function () {

var records = Array.apply(null, new Array(record.length)).map(function (n, i) {

return {

Id: record[i].Id,

Name: record[i].Name,

Country: record[i].BillingCountry,

PostalCode: record[i].BillingPostalCode,

State: record[i].BillingState,

City: record[i].BillingCity,

Street: record[i].BillingStreet,

Phone: record[i].Phone,

Contacts: record[i].Contacts,

Opportunities: record[i].Opportunities

};

});

var template = _.template(str);

for(var i=0; i<records.length; i++){

if(records[i].Id == recordId){

document.getElementsByClassName('detailList')[0].innerHTML = template({records: [records[i]]});

self.setRelatedList(records[i]);

}

}

});

},

setRelatedList: function(record){

var conStr = '<% records.forEach(function (r) { %>¥

<tr>¥

<td>No.<%= r.count %></td><td><%= r.cName %></td><td><i class="fa fa-phone"></i></td><td><i class="fa fa-envelope-o"></i></td>¥

</tr>¥

<% }); %>'

var oppStr = '<% records.forEach(function (r) { %>¥

<tr>¥

<td>No.<%= r.count %></td><td><%= r.oName %></td>¥

</tr>¥

<% }); %>'

function toArray(fakeArray) {

return Array.prototype.slice.call(fakeArray);

}

$(function () {

var template;

if(record.Contacts){

var cRecords = Array.apply(null, new Array(record.Contacts.length)).map(function (n, i) {

return {

count: i+1,

cName: record.Contacts[i].Name

};

});

template = _.template(conStr);

document.getElementsByClassName('conRelatedListBody')[0].innerHTML = template({records: cRecords});

}

if(record.Opportunities){

var oRecords = Array.apply(null, new Array(record.Opportunities.length)).map(function (n, i) {

return {

count: i+1,

oName: record.Opportunities[i].Name

};

});

template = _.template(oppStr);

document.getElementsByClassName('oppRelatedListBody')[0].innerHTML = template({records: oRecords});

}

});

}

})

【AccountSearchStyle.css】

.THIS .section {

padding: 3px;

border: solid 1px #ccc;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

margin: 5px;

}

.THIS .sectionTitile{

background: #717ECD;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

padding: 5px;

color: #fff;

}

.THIS .searchCondition {

padding: 5px;

}

.THIS #selectInputItem{

height: 30px;

min-width: 10em;

border:solid 1px #ccc;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

margin-left: 5px;

}

.THIS #selectSearchItem{

height: 30px;

min-width: 10em;

border:solid 1px #ccc;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

margin-left: 5px;

}

.THIS .searchItem {

display: inline-block;

margin: 5px 10px;

}

.THIS .searchItemLabel{

display: inline-block;

text-align: left;

}

.THIS .searchButtonBox{

display: inline-block;

}

.THIS .searchButton{

height: 30px;

}

.THIS .Name {

font-size: 1.2em;

font-weight: bold;

margin-bottom: 5px;

}

.THIS .wrap {

position: relative;

}

.THIS .recordList {

padding: 10px;

background: #fff;

border:1px solid rgb(199, 199, 199);

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

margin-top:10px;

box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

-webkit-box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

-moz-box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

}

.THIS .mapframe {

position: absolute;

right: 0;

top: 0;

}

.THIS .recordDetail {

padding: 10px;

background: #fff;

border:1px solid rgb(199, 199, 199);

margin-top: 3px;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

-webkit-box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

-moz-box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

}

/* Contact Table */

.THIS .conRelatedList {

margin:10px 0;

}

.THIS .conRelatedList table {

border-collapse: separate;

border-spacing: 1px;

}

.THIS .conRelatedList table thead tr {

background: #56458c;

color: #fff;

}

.THIS .conRelatedList table thead tr th {

padding: 5px;

}

.THIS .conRelatedList table tbody tr td {

padding: 5px;

border-bottom: solid 1px #ddd;

}

/* Opportunity Table */

.THIS .oppRelatedList {

margin:10px 0;

}

.THIS .oppRelatedList table {

border-collapse: separate;

border-spacing: 1px;

}

.THIS .oppRelatedList table thead tr {

background: #F3AE4E;

}

.THIS .oppRelatedList table thead tr th {

padding: 5px;

}

.THIS .oppRelatedList table tbody tr td {

padding: 5px;

border-bottom: solid 1px #ddd;

}

Component

Controller

Helper Style

Copyright © TerraSky Co., Ltd. All Rights Reserved. 9

【AccordionList.cmp】

<aura:component controller="ToDoListController" implements="force:appHostable,flexipage:availableForAllPageTypes">

<aura:attribute name="toDos" type="Task[]" />

<ltng:require styles="

/resource/TS_ToDoList/Font-Awesome-master/css/font-awesome.min.css,

/resource/TS_ToDoList/Swiper-master/dist/css/swiper.min.css"

scripts="

/resource/TS_ToDoList/jquery-1.11.3.min.js,

/resource/TS_ToDoList/lodash.min.js,

/resource/TS_ToDoList/Swiper-master/dist/js/swiper.min.js,

/resource/TS_ToDoList/dateformat.js"

afterScriptsLoaded="{!c.afterScript}"/>

<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

<!-- Todo List -->

<div class="TS_component">

<h2 class="component_Title_header">

<div class="icon_Frame todo_Color"><img src="/img/icon/t4v32/standard/task_120.png" class="icon" alt="ToDo" title="ToDo" /></div>

<span class="component_Title">ToDo List</span>

</h2>

<div class="todo_List" id="todo_List">

</div>

</div>

</aura:component>

【ToDoListController.js】

({

doInit : function(component, event, helper) {

var action = component.get('c.getToDos');

action.setCallback(this,function(response){

component.set('v.toDos',response.getReturnValue());

});

$A.enqueueAction(action);

},

showSpinner : function (component, event, helper) {

var spinner = component.find('todo_spinner');

var evt = spinner.get("e.toggle");

evt.setParams({ isVisible : true });

evt.fire();

},

hideSpinner : function (component, event, helper) {

var spinner = component.find('todo_spinner');

var evt = spinner.get("e.toggle");

evt.setParams({ isVisible : false });

evt.fire();

},

afterScript : function(component, event, helper) {

var action = component.get('c.getToDos');

action.setCallback(this,function(response){

var str = '<% records.forEach(function (r) { %>¥

<div class="swiper-container">¥

<div class="done"><i class="fa fa-check-square-o"></i><br/>Done </div>¥

<div class="delete"><i class="fa fa-trash-o"></i><br />Del </div>¥

<div class="swiper-wrapper <%= r.style %>">¥

<div class="mark_l">¥

<i class="fa fa-caret-left"></i>¥

<i class="fa fa-hand-pointer-o"></i>Done¥

</div>¥

<div class="mark_r">¥

Delete <i class="fa fa-hand-pointer-o"></i>¥

<i class="fa fa-caret-right"></i>¥

</div>¥

<div class="swiper-slide" id="<%= r.Id %>">¥

<div class="subject"><%= r.Name %></div>¥

<span class="details"><%= r.Status %></span>¥

<span class="details"><%= r.ActivityDate %></span>¥

</div>¥

</div>¥

</div>¥

<% }); %>';

helper.setToDoList(component,str,response.getReturnValue(),event);

helper.doSwiper(component);

});

$A.enqueueAction(action);

},

})

【ToDoListHelper.js】

({

setToDoList:function(component, str, record){

var expanded;

var self = this;

function toArray(fakeArray) {

return Array.prototype.slice.call(fakeArray);

}

$(function () {

var records = Array.apply(null, new Array(record.length)).map(function (n, i) {

/* 日付フォーマットM/d/yyyy */

var dateFormat = new DateFormat("M/d/yyyy");

var str = dateFormat.format(new Date(record[i].ActivityDate));

/** 期限を確認する **/

var today = new Date();

var date = new Date(record[i].ActivityDate);

var style = '';

if (today > date) {

style = 'expired';

}

return {

Name: record[i].Subject,

Status: record[i].Status,

IsClosed: record[i].IsClosed,

ActivityDate: str,

Id: record[i].Id,

style: style

};

});

var template = _.template(str);

document.getElementById('todo_List').innerHTML = template({records: records});

});

},

doSaveToDo: function(component, recordId){

// var upsertToDo = {'sobjectType':'Task','Id':recordId,'Status':'Completed'};

var upsertToDo = {'sobjectType':'Task','Id':recordId};

var action = component.get("c.saveToDo");

action.setParams({"tasks":upsertToDo});

action.setCallback(this,function(a){

console.log("FIN!!");

});

console.log("GO!!");

$A.enqueueAction(action);

},

doSwiper: function(component){

var self = this;

var mySwiper = new Swiper('.swiper-container',{

pagination: '.pagination',

loop:false,

paginationClickable:true,

calculateHeight:true,

touchRatio:0.6,

onTransitionStart: function (swiper){

var recordId = swiper.wrapper[0].id;

if(recordId){

self.doSaveToDo(component, recordId);

}

},

onTransitionEnd: function(swiper){

if(swiper.touches.diff <= -170){

$(swiper.container[0]).css('display','none');

} else if (swiper.touches.diff >= 170){

/* $(swiper.wrapper[0]).css({'background':'#D3D3D3','border-color':'#c7c7c7'}); */

$(swiper.wrapper[0]).addClass('todoDone');

/* $(swiper.wrapper[0]).find('.subject').addClass('todoDone'); */

}

}

});

}

})

【ToDoListStyle.css】

/* Component Header */

.THIS .component_Title_header{

margin: 5px;

}

.THIS .icon_Frame {

display: inline-block;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

}

.THIS .todo_Color {

background: #4BC076;

}

.THIS .icon_Frame .icon {

width: 2rem;

height: 2rem;

vertical-align: middle;

}

.THIS .component_Title {

margin-left:10px;

}

/* Records */

.THIS .swiper-wrapper {

height: 100%;

padding: 10px;

/* background: #8BC34A; */

background: #C8E6C9;

background: #8BC34A;

background: #fff;

border:1px solid #388E3C;

margin: 3px;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

-webkit-box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

-moz-box-shadow:rgba(113, 135, 164, 0.65098) 4px 3px 6px -3px;

width:auto;

}

.THIS .mark_l i{

margin-right: 2px;

}

.THIS .mark_l {

position: absolute;

top: 0;

left: 0;

margin-left: 5px;

margin-top: 5px;

color: #fff;

color: #616161;

}

.THIS .mark_r {

position: absolute;

top: 0;

right: 0;

margin-right: 5px;

margin-top: 5px;

color: #fff;

color: #616161;

}

.THIS .swiper-slide {

margin-top:20px;

width:100% !important;

border-left: solid 5px #388E3C;

padding-left: 5px;

}

.THIS .expired {

border:1px solid #D32F2F;

/* background: #eb4654; */

background: #FFCDD2;

background: #FF5252;

background: #eb4654;

background: #fff;

}

.THIS .expired .swiper-slide {

margin-top:20px;

width:100% !important;

border-left: solid 5px #D32F2F;

padding-left: 5px;

}

/* swipe時に表示される */

.THIS .done {

position: absolute;

top: 5px;

padding: 10px;

vertical-align: middle;

background: #4AB471;

color: #fff;

margin-top: 4px;

margin-left: 3px;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

width:50%;

}

.THIS .delete {

position: absolute;

top: 5px;

right: 0;

padding: 10px;

text-align: right;

background: #D96383;

color: #fff;

margin-top: 4px;

margin-right: 3px;

border-radius: 5px;

-webkit-border-radius: 5px;

-moz-border-radius: 5px;

width:48%

}

.THIS .subject {

font-size: 1.2em;

font-weight: bold;

margin-bottom: 5px;

}

.THIS .todoDone {

background: #D3D3D3;

border-color: #c7c7c7;

}

.THIS .todoDone .swiper-slide {

border-left: solid 5px #fff;

}

.THIS .details {

display: inline-block;

color: #fff;

width: 48%;

color: #616161;

}

Component

Controller

Helper

Style

Copyright © TerraSky Co., Ltd. All Rights Reserved. 10

[開発者向け] Lightning コンポーネント開発の勘所

9-4

Copyright © TerraSky Co., Ltd. All Rights Reserved. 11

• 全般– Lightningの進化速度

• Github : aura ソースコードについて

• コーディング– Locker Service に気をつけろ!

• React利用で困ったこと

– SPAの考慮点• ブラウザバック• LightningアプリケーションとLightning Experience

– ファイルアップロードの開発は止めた方が良い

• パフォーマンス問題– 表示切り替えで<aura:if>は使ってはいけない

• https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/components_conditional_markup.htm

– initイベントとrendering• https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_cb_init_handler.htm?search_text=invoking%20actions%20on%20component

Copyright © TerraSky Co., Ltd. All Rights Reserved. 12

Developer の仕事

Copyright © TerraSky Co., Ltd. All Rights Reserved. 13

デベロッパとは、「開発(develop)する者」を意味する一般的な英語であり、IT用語としては、主にソフトウェアの製作を手がける事業者を指す語として用いられる。

傾向としては、デベロッパの語は事業者、開発に携わる組織全体を指す語として用いられる。個人を指して「プログラマ」や「エンジニア」と同じ意味合いでデベロッパと呼ぶ場合もあるが、デベロッパは必ずしもプログラミングを手がける個人を意味するとは限らない。

なお、不動産関連の分野では、宅地の造成を手がける事業者をデベロッパという。

「IT用語辞典バイナリ」より引用

http://www.sophia-it.com/content/developer

Copyright © TerraSky Co., Ltd. All Rights Reserved. 14

プロトタイプ開発

(設定)

Salesforceの設定

デモ

プロトタイプ開発(コーディング)

Visualforce、Apexクラス、Apexトリガー 設計

コーディング

提案

UT

IT

ST

UAT

Fit & Gap(設定 or 開発)

Fit & Gap(利用 or スクラッチ)

見積り 要件定義 設計・開発 テスト 移行 本番稼働

見積書作成 要件定義書作成データ移行

変更セット

Copyright © TerraSky Co., Ltd. All Rights Reserved. 15

プロトタイプ開発

(設定)

Salesforceの設定

デモ

プロトタイプ開発(コーディング)

Visualforce、Apexクラス、Apexトリガー 設計

コーディング

提案

UT

IT

ST

UAT

Fit & Gap(設定 or 開発)

Fit & Gap(利用 or スクラッチ)

見積り 要件定義 設計・開発 テスト 移行 本番稼働

見積書作成 要件定義書作成データ移行

変更セット

Fit & Gap(利用 or スクラッチ)

Lightning、Apexクラス、Apexトリガー 設計

コーディング

既存(Visualforce、Apex)開発とLightning開発の差分

Copyright © TerraSky Co., Ltd. All Rights Reserved. 16

良い設計が

できる人

Copyright © TerraSky Co., Ltd. All Rights Reserved. 17

プロトタイプ開発

(設定)

Salesforceの設定

デモ

プロトタイプ開発(コーディング)

Visualforce、Apexクラス、Apexトリガー 設計

コーディング

提案

UT

IT

ST

UAT

Fit & Gap(設定 or 開発)

Fit & Gap(利用 or スクラッチ)

見積り 要件定義 設計・開発 テスト 移行 本番稼働

見積書作成 要件定義書作成データ移行

変更セット

Fit & Gap(利用 or スクラッチ)

Lightning、Apexクラス、Apexトリガー 設計

Fit & Gap(設定 or 開発)

Fit & Gap(利用 or スクラッチ)

Lightning、Apexクラス、Apexトリガー 設計

コーディング

Copyright © TerraSky Co., Ltd. All Rights Reserved. 18

コード

アプリケーション

ページページページ

コンポーネントコンポーネント コンポーネント

クラス/ファイル

メソッド/ファンクション

Copyright © TerraSky Co., Ltd. All Rights Reserved. 19

まとめ

Copyright © TerraSky Co., Ltd. All Rights Reserved. 20

• Developer は実施する作業が沢山ありますコーディングだけが仕事ではない

• 設計が重要

⁃ Salesforce標準設定 と スクラッチ開発

⁃ 既存アプリ、コンポーネント と スクラッチ開発

⁃ コンポーネントの切り分け

• Lightningコンポーネントを作成する「SuPICE」という製品もあります

D-1

ハンズオン会場

photoカウンター

シアター