Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Hotwire or React? ~Reactの録画機能をHotwireに置き換えて得られた...
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
haruna tsujita
October 25, 2024
Programming
10k
11
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Hotwire or React? ~Reactの録画機能をHotwireに置き換えて得られた知見~ / hotwire_or_react
haruna tsujita
October 25, 2024
More Decks by haruna tsujita
See All by haruna tsujita
Rebuilding Turbo Streams with ruby.wasm and Ruby Sockets
harunatsujita
1
2k
Hotwire or React? ~アフタートーク・本編に含めなかった話~ / Hotwire or React? after talk
harunatsujita
1
290
fbc-graduation-napple
harunatsujita
0
120
Rails Girls 2022 LT
harunatsujita
0
650
はじめてのしくじり /fjordbootcamp-211009
harunatsujita
0
1.2k
Other Decks in Programming
See All in Programming
New "Type" system on PicoRuby
pocke
1
970
OSもどきOS
arkw
0
570
代数的データ型って何が嬉しいの? #frontend_phpcon_do
kajitack
8
3.7k
Observability in Practice:Grafana 與 Edge Device SRE 的那些事
blueswen
0
170
Agentic UI
manfredsteyer
PRO
0
180
Strategic Design in the Frontend: Moduliths & Micro Frontends @DDDEurope
manfredsteyer
PRO
0
110
技術記事、 専門家としてのプログラマ、 言語化
mizchi
13
6.2k
JJUG CCC 2026 Spring: JSpecify で実現する Kotlin フレンドリーな Java API 設計
ternbusty
1
180
Snowflake Summitでの新機能 CoCo / CoWork / snowflake-summit-2026-overall-what-new-coco
tatsuhiro
1
150
不変条件と整合性境界—ビジネスが決める設計判断と実現パターン / Invariants and Consistency Boundaries
nrslib
14
5.6k
Lemonade + Foundry Toolkit でお手軽アプリ開発
seosoft
1
360
jQueryをバージョンアップする前に使いたいjQuery Migrate
matsuo_atsushi
0
560
Featured
See All Featured
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
Test your architecture with Archunit
thirion
1
2.3k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.4k
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
210
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
128
56k
Fantastic passwords and where to find them - at NoRuKo
philnash
52
3.7k
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.1k
New Earth Scene 8
popppiees
3
2.3k
Un-Boring Meetings
codingconduct
0
320
Why Our Code Smells
bkeepers
PRO
340
58k
How to Think Like a Performance Engineer
csswizardry
28
2.7k
Building an army of robots
kneath
306
46k
Transcript
)BSVOB5TVKJUB !IBSVOBUTVKJUB ,BJHJPO3BJMT )PUXJSFPS3FBDU d3FBDUͷըػೳΛ)PUXJSFʹஔ͖͑ͯಘΒΕͨݟd
ࣗݾհ !IBSVOBUTVKJUB ✦ˠגࣜձࣾΩϟλϧʢӳޠक़ΛӡӦʣ ✦όοΫΤϯυΤϯδχΞʢ3BJMTͱɺͨ·ʹ3FBDUʣ ✦ϥΠςΟϯάͷఴαʔϏεΛ։ൃ
ࠓͷςʔϚ ✦ϝΠϯ)PUXJSFɺಛʹ4UJNVMVTͷͰ͢ ✦ٕज़બఆʹؔ͢Δ͠·͢ w ࢲ3FBDU͖Ͱ͢ w ຊͷલʹϓϩμΫτνʔϜͷঢ়گΛ؆୯ʹڞ༗͠·͢
ϥΠςΟϯάͷఴαʔϏεͱʁ w ӳޠक़Ωϟλϧʹ௨͏ੜెʹఏڙ͍ͯ͠ΔখنͳΞϓϦέʔγϣϯ w 3VCZPO3BJMT෦తʹ3FBDUͰ41"Λ࣮ݱ ڭࢣ ੜె ӳޠͰϥΠςΟϯάΛॻ͘ɾఏग़ ఴɾϑΟʔυόοΫ
3FBDUɺຊʹඞཁʁ ͜ͷ͘Β͍ͷنͷαʔϏεʹ
w 3BJMTͷํ͕ಘҙͳόοΫΤϯυΤϯδχΞ໊͕։ൃ w ޠΧϯτɾςΩετΤϦΞͷϋΠϥΠτͳͲͪΐͬͱͨ͠ՕॴΛ41"ʹ͍ͨ͠ ‣3FBDUͰॻ͔ΕͨՕॴ͕ঃʑʹෛ࠴Խ ‣ϝΠϯͷػೳ։ൃʹ͕͔͔࣌ؒͬͯ͠·͏ w Γ͍ͨ͜ͱ)PUXJSFͰे࣮ݱͰ͖ΔͷͰʁ 3FBDUؔ࿈ͷ՝
.ZOBNFJT)BSVOB *`NGSPN5PLZP େֻ͔Γͳ͜ͱ͍ͯ͠ͳ͍ʝ
3FBDUΛΊͯ)PUXJSFʹ͢Δͷා͍ʂʂʂ
ා͞ͷਖ਼ମʢͱࢥΘΕΔͷʣ 3FBDU ΤίγεςϜݟ͕ॆ࣮ʢͱ͘ʹ$IBLSB6*Λ͍͍ͨʣ ॊೈʹ6*Λ࣮Ͱ͖Δ )PUXJSF 5VSCPͷΠϝʔδɾ$36%ૢ࡞Λ/P+4Ͱ41"ϥΠΫʹͰ͖Δ ཧػೳ͘Β͍ͷϓϩμΫτʹϑΟοτʁ 4UJNVMVTͷใ͕͋·Γͳͯ͘ະ
)PUXJSFͷΈͰे։ൃͰ͖Δʁ3FBDUΛख์ͯ͠ຊʹ͍͍ͷʁ
ٕज़ݕূ͢ΔՁ͋Γͦ͏ʂ w ݒ೦ͷҰͭ4UJNVMVT ‣͜Ε͕ϑΟοτ͠ͳ͚Ε)PUXJSFஔ͖͑ͷબࢶ͕ফ͑Δ w 3FBDUΛؤுΖ͏ʂͱܾҙͰ͖ΔͷνʔϜʹͱͬͯϓϥεͳͣ
ըػೳΛஔ͖͑ͯΈΔ w ϑΟʔυόοΫಈըͷըػೳ ‣ಈըσʔλͷอଘɾ࠶ੜɾআ ‣1$ͷΧϝϥɾϚΠΫͷૢ࡞͕ೖΔͨΊ ࣮֬ʹ4UJNVMVTΛ͏ ‣ػೳվળ͕׆ൃʹߦΘΕͳ͍Օॴ 3FDPSE $MFBS
ஔ͖͑ͷલఏ݅ w 3FBDUͱ)PUXJSFͷҧ͍Λݕূ͢Δ͜ͱ͕త ‣طଘͷ6*69Λҡ࣋͠ɺ༷มߋ͠ͳ͍
3FDPSE $MFBS
3FDPSE $MFBS 4UPQ $MFBS )J 3FDPSEˠ4UPQ ըελʔτ
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
ಈը࠶ੜ EJTBCMFEղআ EJTBCMFEUSVF 1045
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
1045 %&-&5&
3FBDUͰͷ࣮Λݟ͍͖ͯ·͠ΐ͏
ݱঢ়ͷ࣮ 3FBDU ఴϖʔδ 3FDPSE $MFBS // app/views/summaries/feedbacks/edit.html.haml -# লུ #summary-video-recorder{data:
{'summary-id': @summary.id}} = javascript_include_tag ‘SummaryVideoRecorder' -# লུ w ఴϖʔδͷWJFX͔Β3FBDUΛݺͼग़͢
w ಈըͷදࣔɾ3FDPSEϘλϯɾ$MFBSϘλϯ͕ίϯϙʔωϯτԽ w ϘλϯͷΫϦοΫΠϕϯτΛىʹॲཧ͕࣮ߦ͞ΕΔ return ( <> <Video theme='large' videoUrl={videoUrl}
mimeType='video/webm' /> <p> <RecordButton startRecording={startRecording} stopRecording={stopRecording} id={summaryId} videoUrl={videoUrl} /> <ClearButton onClick={handleClear} isDisabled={isPosting || !videoUrl} /> </p> </> ) } ݱঢ়ͷ࣮ 3FDPSE $MFBS
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL 5VSCPͱ૬ੑͷΑ͍ +BWB4DSJQUϑϨʔϜϫʔΫ
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL ΠϕϯτͰൃੜ͢ΔΞΫγϣϯΛࢦఆ
)PUXJSF4UJNVMVTͱʁ <!--HTML from anywhere--> <div data-controller="hello"> <input data-hello-target="name" type="text"> <button
data-action="click->hello#greet"> Greet </button> <span data-hello-target="output"> </span> </div> // hello_controller.js import { Controller } from "stimulus" export default class extends Controller { static targets = [ "name", "output" ] greet() { this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!` } } Ҿ༻ɿ)PUXJSF)BOECPPL )BSVOBͱೖྗͯ͠(SFFUϘλϯΛԡ͢ͱz)FMMP )BSVOBz͕ग़ྗ
)PUXJSFஔ͖͑ // app/javascript/controllers/video_recorder_controller.js export default class extends Controller { static
targets = ["video"] static values = { summaryId: Number } } -# app/views/summaries/feedbacks/edit.html.haml %div{data: {controller: "video-recorder", video_recorder_summary_id_value: @summary.id}} %video{data: {video_recorder_target: "video"}, controls: true, autoplay: true, muted: true} %button{data: {action: "click->video-recorder#startRecording"}} Record %button{data: {action: "click->video-recorder#stopRecording"} Stop %button{data: {action: "click->video-recorder#clearVideo"} Clear
)PUXJSFஔ͖͑ ్࣮தͳͷʹɺͳΜ͔+BWB4DSJQU ͍ͬͺ͍ॻ͍ͯΔͧɾɾʁ // app/javascript/controllers/video_recorder_controller.js import { Controller } from
"@hotwired/stimulus" // Connects to data-controller="feedback-video" export default class extends Controller { static targets = ["video", "recordButton", "stopButton", "clearButton"] static values = { summaryId: Number } async connect() { console.log(this.summaryIdValue) this.chunks = [] const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) this.videoTarget.srcObject = stream this.videoTarget.play() this.mediaRecorder = new MediaRecorder(stream) this.mediaRecorder.ondataavailable = (event) => this.chunks.push(event.data) } startRecording() { try { console.log('start recording') this.mediaRecorder.start() } catch(error) { console.error('Error accessing media devices.', error) } } stopRecording() { console.log('stop recording') this.mediaRecorder.stop() this.mediaRecorder.onstop = this.handleStop.bind(this) } handleStop() { const blob = new Blob(this.chunks, { type: 'video/webm' }) const url = URL.createObjectURL(blob) this.videoTarget.src = url this.uploadVideo(blob) } async uploadVideo(blob) { const formData = new FormData() formData.append( 'summary_video[video]', blob, `summary_video_id${this.summaryIdValue}.webm` ) const response = await fetch(`/api/summaries/${this.summaryIdValue}/summary_feedback_videos`, { method: 'POST', body: formData, headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }) if (response.ok) { await response.json() console.log('Video uploaded') } else { console.error('Failed to upload video', await response.text()) } } clearVideo() { try { fetch(`/api/summaries/${this.summaryIdValue}/summary_feedback_videos`, { method: 'DELETE', headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }) } catch(error) { console.error('Error deleting video', error) } } } ˞தಡ·ͳͯ͘େৎͳͷͰɺ ࠞಱͱͨ͠ײ͡ΛΈऔ͍͚ͬͯͨͩΔͱخ͍͠Ͱ͢ɻ
4UJNVMVTΛ৮ͬͯΈͨॴײ w ಈ࡞࣮ݱͰ͖ͦ͏ɻ w 4UJNVMVTࣗମʹॻ͖ͮΒ͞ͳ͍ɻ w Ͱɺͬ͘͠Γ͜ͳ͍ɻ ͱɺࢥ͍ͭͭνʔϜϝϯόʔʹ్தܦաΛڞ༗ͨ͠ͱ͜Ζɾɾɾ
ΰϦΰϦʹ+BWB4DSJQUॻ͘͜ͱʹͳͬͪΌ͏Ͷ ͜ΕͳΒ3FBDUͰྑ͍ͷͰʁ
ϓϩϙʔβϧఏग़࣌͜͏݁Λग़͍ͯͨ͠
ຊʹ͜ΕͰɺྑ͍ͷ͔ɾɾʁʁʁ
ʢ࠶ܝʣ4UJNVMVTΛ৮ͬͯΈͨॴײ w ಈ࡞࣮ݱͰ͖ͦ͏ɻ w 4UJNVMVTࣗମʹॻ͖ͮΒ͞ͳ͍ɻˠ w Ͱɺͬ͘͠Γ͜ͳ͍ɻ ͱɺࢥ͍ͭͭνʔϜϝϯόʔʹ్தܦաΛڞ༗ͨ͠ͱ͜Ζɾɾɾ
w 3FBDUʹؔ͢ΔࣝʢVTF4UBUFͳͲͷ)PPLTʣ͕ඞཁͳ͍ ‣)PPLT͕Α͠ͳʹॲཧ͍ͯͨ͠ՕॴΛࣗͰ࣮͢Δ͜ͱʹ w ָʹϑϩϯτΤϯυΛॻ͖͔ͨͬͨͣͳͷʹ 3FBDUΛ༻͢ΔΑΓ+BWB4DSJQUͷίʔυ͕૿Ճ ॻ͖ͮΒ͕͞ͳ͍ɺͷਖ਼ମʁ
վળͷ༨Λ୳Δ w 4UJNVMVTίϯτϩʔϥʔʹաͳ+BWB4DSJQUͷϩδοΫ͕ूத ‣ͦΕͳΒ3FBDU͕ྑ͍ w ͔͠͠ɺผͷՕॴͰ5VSCPΛ࣮ͬͯͨ͠ͱ͖ ‣5VSCPνʔϜͰධͩͬͨ
࣮ํΛݟ͢ w Ծઆ̍ɿ)PUXJSFͷࢫΈ5VSCPͳͷͰ ‣5VSCPʹدͤΔ͜ͱͰɺ4UJNVMVTʹॻ͘+BWB4DSJQU͕ݮΔͣ w Ծઆ̎ɿ4UJNVMVTͷίϯτϩʔϥʔΛཧ͢Δͱҹ͕ҧ͏ͷͰ
)PUXJSF5VSCPͱʁ w $36%ૢ࡞Λத৺ͱͨ͠41"ϥΠΫͳମݧΛ+BWB4DSJQUΛॻ͔ͣʹ ࣮ݱͰ͖Δπʔϧ ങ͍ʹߦ͘ ݘʹ㕒Λ͋͛Δ
)PUXJSF5VSCPͱʁ w ϦΫΤετʹରͯ͠ɺαʔόʔࢦఆͨ͠Օॴͷ)5.-ΛϨεϙϯε ‣ը໘શମΛ࠶ඳը͠ͳ͍ͷͰ41"ϥΠΫͳಈ࡞Λ࣮ݱͰ͖Δ ങ͍ʹߦ͘ ݘʹ㕒Λ͋͛Δ
ըػೳͷͲ͜Ͱ5VSCPΛ͑Δʁ
˞ҎԼ,BJHJPO3BJMTαΠτ͔ΒҰ෦ൈਮ 41"ͱ)PUXJSFͰɺΞϓϦΛߏ͢ΔͨΊͷͷߟ͑ํ͕େ͖͘ҟͳΓ·͢ɻ )PUXJSFΛॎԣແਚʹ͍ͨ͢Ίʹɺ)PUXJSFతͳߟ͑ํͰ࡞Δඞཁ͕͋ΔͷͰ͢ɻ ͜ͷɺ)PUXJSFతͳߟ͑ํʹઃܭࢦʹࢲʮ8FCࢴࣳډʯͱ͍͏໊લΛ͚ͭ·ͨ͠ɻ
3FDPSE $MFBS 4UPQ $MFBS )J 4UPQ $MFBS (PPEMVDL 3FDPSE $MFBS
1045 %&-&5& ը໘ͷྲྀΕʹԊ࣮ͬͯ͢Δͷμϝͩͬͨɾɾ
ߟ͑ํΛม͑ͯΈΔʂ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ʢ࠶ܝʣϘλϯͷΫϦοΫͰঢ়ଶ͕ભҠ͢Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& %#ʹಈը σʔλͳ͠ %#ʹಈը σʔλ͋Γ ը։࢝ ऴྃ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 5VSCPΛར༻Ͱ͖ͦ͏
ࢴࣳډͱͯ͠ߟ͑ͯΈΔ 3FDPSE ᶃಈըσʔλͳ͠ $MFBS ᶄಈըσʔλ͋Γ 1045 %&-&5& $MFBS 3FDPSE w
$36%ૢ࡞͕͖͔͚ͬͰൃੜ͢Δը໘ͷΓସ͑5VSCPʹͤΔ ˞ʰ8FCࢴࣳډʱͷτʔΫը໘શମΛຕͷࢴࣳډͱͯ͠ଊ͑Δ͓ͳͷͰɺ τʔΫ͔ΒώϯτΛಘͨ͏͑ͰҟͳΔΞϓϩʔνΛ͍ͯ͠·͢
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ ʁʁʁ ը։࢝ऴྃ Ϙλϯͷ%0.ૢ࡞ͳͲ
ࢴࣳډͱͯ͠ߟ͑ͯΈΔᶄ 4UPQ ᶃ $MFBS 3FDPSE w lࢴࣳډͷֻ͚zͱଊ͑ͯΈΔ ‣3BJMTଆͰ੍ޚ͢Δͷ͍͠ ‣4UJNVMVTͰ࣮͢Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 5VSCPͰ࣮
)PUXJSFஔ͖͑d5VSCPΛ͏d // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder",
{//stimulusίϯτϩʔϥσʔλ͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ // ఴϖʔδͷview = render partial: 'summaries/feedbacks/summary_feedback_video', locals: { // লུ } ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX
// app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {//
ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? %video %source{src: summary_feedback_video.video_url, type: "video/webm"} = link_to 'Clear', summary_summary_feedback_videos_path(summary), data: { turbo_confirm: "ຊʹআ͠·͔͢ʁ", turbo_method: :delete } - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ ˞εϖʔεͷؔͰࢿྉͰҰ෦লུ͍ͯ͠ΔՕॴ͕͋Γ·͢ # app/controllers/summaries/summary_feedback_videos_controller.rb def destroy @summary = Summary.find(params[:summary_id]) @summary_feedback_video = @summary.summary_feedback_video @summary_feedback_video.destroy! end ˢ҉తʹUVSCP@TUSFBNΛSFOEFS ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔ
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBN ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX ˞εϖʔεͷؔͰࢿྉͰվߦΛೖΕ͍ͯ·͢
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBN // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {// ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ ઌ΄Ͳ·Ͱσʔλ͕ ͋ͬͨͷͰͪ͜Β
// app/views/summaries/summary_feedback_videos/destroy.turbo_stream.haml = turbo_stream.replace 'summary-feedback-video', partial: 'summaries/feedbacks/summary_feedback_video', locals: { summary:
@summary, summary_feedback_video: nil } ˠ5VSCPϦΫΤετˠ3BJMTίϯτϩʔϥʔˠUVSCP@TUSFBNˠ // app/views/summaries/feedbacks/_summary_feedback_video.html.haml = turbo_frame_tag 'summary-feedback-video' do %div{data: {controller: "video-recorder", {// ίϯτϩʔϥʔσʔλड͚͠}}} .is-flex.is-flex-direction-column.is-align-items-start - if summary_feedback_video.present? // ϏσΦσʔλ͕͋Δ࣌ͷॲཧ - else // ϏσΦσʔλ͕ͳ͍࣌ͷॲཧ σʔλ͕আ͞Εͨ ͷͰͪ͜Β͕࣮ߦ͞ΕΔ ˣbTVNNBSZGFFECBDLWJEFP` ఴϖʔδͷWJFX
3FDPSE ᶃಈըσʔλͳ͠ $MFBS ᶄಈըσʔλ͋Γ 1045 %&-&5& $MFBS 3FDPSE ʢ࠶ܝʣࢴࣳډͷجૅ͕ग़དྷ্͕Δ
3FDPSE $MFBS 4UPQ $MFBS (PPEMVDL 4UPQ $MFBS )J 3FDPSE $MFBS
1045 %&-&5& ᶃಈըσʔλͳ͠ ᶄಈըσʔλ͋Γ 4UJNVMVTͰ࣮
ըʹؔ͢ΔॲཧτʔΫͷຊےͰͳ͍ͷͰɺ ίʔυΛཧͨ͠ํ๏ʹ͍͓ͭͯ͠͠·͢
ʢ࠶ܝʣ࣮ํΛݟ͢ w Ծઆ̍ɿ)PUXJSFͷࢫΈ5VSCPͳͷͰ ‣5VSCPʹدͤΔ͜ͱͰɺ4UJNVMVTʹॻ͘+BWB4DSJQU͕ݮΔͣ w Ծઆ̎ɿ4UJNVMVTͷίϯτϩʔϥʔΛཧ͢Δͱҹ͕ҧ͏ͷͰ ‣ϦϑΝΫλϦϯάํ๏·ͩࡧதͰ͕͢ɾɾ
// app/javascript/controllers/video_recorder_controller.js import { Controller } from "@hotwired/stimulus" import {
// ར༻͢Δؔ } from “./video_recorder_utils" export default class extends Controller { static targets = ["video", "recordButton"] static values = { writingId: Number, writingType: String, feedbackVideo: String, uploadUrl: String } async connect() { // ଓ࣌ͷॲཧ } async startRecording() { // ը։࢝ͷॲཧ } stopRecording() { // ըऴྃͷॲཧ } }
// app/javascript/controllers/video_recorder_controller.js import { Controller } from "@hotwired/stimulus" import {
initializeMediaStream, setupMediaRecorder, setStopButton, uploadData } from “./video_recorder_utils" // Connects to data-controller="video-recorder" export default class extends Controller { static targets = ["video", "recordButton"] static values = { writingId: Number, writingType: String, feedbackVideo: String, uploadUrl: String } async connect() { if (!this.feedbackVideoValue) { this.mediaStream = await initializeMediaStream() } } async startRecording() { if (!this.mediaStream) { this.mediaStream = await initializeMediaStream() if (!this.mediaStream) return } this.mediaRecorder = setupMediaRecorder(this.mediaStream) this.mediaRecorder.start() setStopButton(this.recordButtonTarget) this.mediaRecorder.ondataavailable = (event) => uploadData(event, this.writingTypeValue, this.writingIdValue, this.uploadUrlValue) } stopRecording() { this.mediaRecorder?.stop() this.mediaStream.getTracks().forEach(track => track.stop()) this.recordButtonTarget.disabled = true }} 5PUBMߦ
͜ΕͰஔ͖͑ྃͰ͢ʂ
13Ϛʔδ͞Ε·ͨ͠🎉 ٞͷ݁Ռʜ
13Ϛʔδ͞Ε·ͨ͠🎉 ˞ࠩ Ͱ͕͢ɺఴը໘ͷըػೳՕॴͷஔ͖͑Λ͓͜ͳ͍ɺ 3FBDUଆͰڞ௨Խ͞Ε͍ͯͳ͔ͬͨՕॴ͕ࠓճڞ௨Խ͞Εͯͷ݁ՌͳͷͰ Օॴͷ߹ɺݮগྔఔʹऩ·Γ·͢
)PUXJSFPS3FBDUʁ
$36%ͷΈ $36% +4 $36%ͳ͠ +4 )PUXJSFPS3FBDUʁ w $36%ૢ࡞͕ओମ ิॿతͳ+BWB4DSJQU͕ඞཁͳը໘ ‣ࢴࣳډ
ͪΐͬͱֻ͚͕ͨ͋͠Δ߹ w )PUXJSFͰेରԠͰ͖Δɺੵۃతʹ͍͖͍ͬͯͨ
w $36%ૢ࡞ΛΘͣϦονͳΠϯλϥΫγϣϯ͕ඞཁͳը໘ ‣FYෳࡶͳεςʔτཧ͕ඞཁ w ࢲͷ୲ϓϩμΫτʹ֘͠ͳ͍ ‣3FBDUͷಘҙ $36%ͷΈ $36% +4 $36%ͳ͠
+4 )PUXJSFPS3FBDUʁ
w $36%ΛؚΉߴͳ41"࣮͕ඞཁͳը໘ w $36%ͳ͠Ͱγϯϓϧͳ41"࣮͕ඞཁͳը໘ ‣୲ϓϩμΫτͰςΩετΤσΟλ͕֘ ‣͜ͷ͋ͨΓҙݟ͕͔ΕΔ )PUXJSFPS3FBDUʁ $36%ͷΈ $36% +4
$36%ͳ͠ +4
w $36%ͳ͠Ͱ41"࣮͕ඞཁͳը໘ ‣ଟ͘Ϣʔβʔ͕࣌ؒࡏ͢Δϖʔδ ‣ຕͷࢴࣳډͷதʹֻ͚͕ͨ͘͞Μ͋Δॴ )PUXJSFPS3FBDUʁ $36%ͷΈ $36% +4 $36%ͳ͠ +4
)PUXJSFPS3FBDUʁ $36%ͳ͠ͷ 41"࣮Λ͢Δ߹ ✦3FBDU w )PPLTΤίγεςϜͷਅՁΛൃش࢝͠ΊΔ ‣͔͠͠3FBDUʹର͢Δਂ͍ཧղ͕ඞཁ ✦)PUXJSF w αʔόʔαΠυʹ࣮ΛدͤΒΕͯѻ͍͍͢
‣࣮ͷɾ)PUXJSFతઃܭ ࣮Λ$36%ʹམͱ͠ࠐΉඞཁ͕͋Δ $36%ͷΈ $36% +4 $36%ͳ͠ +4
)PUXJSFPS3FBDUʁ w 3FBDUͱ)PUXJSFɺͲͪΒҰҰ͋Γɺબ͢Δͷ͍͠ w ͨͩ͠ɺʰ)PUXJSFͰ࣮Ͱ͖ͦ͏͔ʁʱͱ͍͏ࢹͩͱ w ࣮Λ$36%ʹूͰ͖Δ͔Ͳ͏͔ɺ͕େ͖ͳϙΠϯτ
✦)PUXJSFతͳઃܭ͕Ͱ͖Εɺద༻ՄೳͳྖҬ૾ΑΓ͍ ✦)PUXJSFΛͬͯΈΑ͏͔ͳɺͱࢥ͏ํ͕૿͑ͨΒخ͍͠Ͱ͢ʂ ·ͱΊ
͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ʂ
ࢀߟ w )PUXJSF)BOECPPLIUUQTIPUXJSFEEFW w 5VSCP)BOECPPLʢຊޠ༁ʣIUUQTFWFSZMFBGHJUIVCJP IPUXJSF@KBUVSCPIBOECPPLJOUSPEVDUJPO w ೣͰΘ͔Δ)PUXJSFೖ5VSCPฤIUUQT[FOOEFWTIJUB CPPLTDBUIPUXJSFUVSCP w
)PUXJSFతͳઃܭΛٻͯ͠ʮ8FCࢴࣳډʯʹߦ͖ண͍ͨIUUQT LBJHJPOSBJMTPSHUBMLTOBZ