【コピペだけ】NuxtJSでCodePenのクローンを作成しよう

皆さん、[CodePen](https://codepen.io/)は使ったことがありますか？CodePenはHTML、CSS、JavaScriptを入力してその結果をブラウザで見れるウェブアプリになります。CodePenはCSSやJavaScriptを使ってアニメーションを探したりするときにつかったりします。

今日はシンプルなCodePenのクローンを作成したので紹介します。

CodePenの例：



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3001224/9b0d6b4d-1379-db9a-f252-a8d5023f6883.png)



今日は自作でJavaScript、HTML、CSSを入力できるエディターとその結果が表示されるアプリのベースを作っていきます。

完成形は画像のようになります。[完成したコードはこちらのGitHubリポ](https://github.com/TraitOtaku/js-builder)からどうぞ。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3001224/2a4129ff-2ba1-0413-a634-49a8f5523754.png)




## **この記事を読む前に知っておくこと**



* JavaScriptの基礎知識
* [Vue](https://asameshicode.com/vue-crashcourse/)、[Nuxtの基礎知識](https://asameshicode.com/how-to-start-nuxt-js/)
* [gitのコマンド](https://youtu.be/yaDt2qnYNKM)


## **このコードで使用するテクニック**



* iFrameのsrcdocNuxtのHTML内にユーザー用のHTMLドキュメントを作成する
* [Monaco EditorでVSCodeのエディターを作成](https://asameshicode.com/vite-monaco-editor/)
* ComputedでJavaScript、HTML、CSSに変更があった都度に結果をレンダー
* PrimeVueのUIコンポーネントをSSRで使用


## **リポジトリのクローン**

では下記のコマンドを使用してリポジトリからデモアプリをセットアップします。


```bash
git clone git@github.com:TraitOtaku/js-builder.git
cd js-builder
npm i
//VSCode
code .
npm run dev
```



## **ファイル構成**

**nuxt.config.ts**

Nuxtのコンフィグファイルです。

monaco editorのモジュールの登録と、PrimeVueのUIライブラリで使うスタイルのインポートをしています。


```js
export default defineNuxtConfig({
 modules: [
   'nuxt-monaco-editor'
 ],
 css:[
   'primevue/resources/themes/saga-blue/theme.css', //テーマカラーは色々あります。
   'primevue/resources/primevue.min.css',
   'primeicons/primeicons.css',
   '~/assets/css/reset.css', //Custom Prime Flex
   '~/assets/css/primeflex.css', //Custom Prime Flex
 ],
 build: {
   transpile: ['primevue']
 }
})
```


**plugins/primevue.js**

pluginsディレクトリはNuxtで指定されているディレクトリでレンダー時にNuxtが自動で読み込んでくれます。ここでは、グローバルで使えるPrimeVueのコンポーネントを登録しています。

詳しい[PrimeVueのセットアップの仕方の記事はこちら](https://asameshicode.com/nuxt3-primevue/)から。

**pages/code.vue**

pagesも同じようにNuxtで指定されている、自動で読み込んでくれるディレクトリです。pages/index.vueがランディングページになります。

/codeのページに行くとcode.vueのページ（コンポーネント）が読み込まれます。

**components/code/editor.vue**

componentsディレクトもNuxtで指定されているディレクトリになります。

ここで登録されたvueファイルはグロバーバルで&lt;CodeEditor>のように使用することができます。ファイル名の最初が大文字になることと、componetsディレクトリからディレクトリ名を当ててパスをしてするような方法になります。

[TabViewとTabPanel](https://primevue.org/tabview/)：PrimeVueのコンポーネントです。

MonacoEditor：[monaco editorのNuxtライブラリで使えるコンポーネント](https://github.com/e-chan1007/nuxt-monaco-editor)です。

valueJS、valueHTML、valueCSS：初期値とユーザーが入力できるリアクティブな値

iFrameのsrcdoc：iframeDoc変数（computedのHTMLファイルをレンダーします）

monacoConfig：テキストエディターのパラメーター（ダークテーマ、ミニマップの表示、行数の表示など）


```js
<template>
 <div class="flex-row">
   <div class="flex flex-wrap">
     <div class="col-12 md:col-6">
       <TabView>
         <TabPanel header="JavaScript">
           <MonacoEditor
             class="monaco-editor-container"
             lang="javascript"
             v-model="valueJS"
             :options="monacoConfig"
           />
         </TabPanel>
         <TabPanel header="HTML">
           <MonacoEditor
             class="monaco-editor-container"
             lang="html"
             v-model="valueHTML"
             :options="monacoConfig"
           />
         </TabPanel>
         <TabPanel header="CSS">
           <MonacoEditor
             class="monaco-editor-container"
             lang="css"
             v-model="valueCSS"
             :options="monacoConfig"
           />
         </TabPanel>
       </TabView>
     </div>
     <div class="col-12 md:col-6">
       <TabView>
         <TabPanel header="Result">
           <div class="iframe-container">
             <iframe
               :srcdoc="iframeDoc"
               frameborder="0"
               class="iframe-result h-full w-full"


             ></iframe>
           </div>
         </TabPanel>
         <TabPanel header="console"> </TabPanel>
       </TabView>
     </div>
   </div>
 </div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
const valueJS = ref(`
var d = new Date();
var monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ];
var date = document.getElementById("date");
var time = document.getElementById("time");
function getDate() {
   date.innerHTML = monthNames[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
}
function timer() {
   setTimeout(timer, 1000);
   var d = new Date();
   var hours = d.getHours();
   var minutes = d.getMinutes();
   var ampm = hours <= 11 ? 'am' : 'pm';
   var strTime = [hours % 12,
                 (minutes < 10 ? "0" + minutes : minutes)
                 ].join(':') + ampm;
   time.innerHTML = strTime;
   setTimeout(timer, 1000);
}
getDate();
timer();
`);
const valueHTML = ref(`
   <div class="wrapper">
       <div class="content">
           <h1 id="date" class="date"></h1>
           <h3 id="time" class="time"></h3>
       </div>
   </div>


`);
const valueCSS = ref(
 `
 * {
   margin: 0;
   padding: 0;
}
body {
   background: #fff;
   font-family: lato,sans-serif;
   color: #bdc3c7;
}
.wrapper {
   width: 400px;
   margin: 10% auto;
}
.content {
   background: #fff;
   box-shadow: 0 0 10px rgba(0,0,0,0.5);
   width: 400px;
}
.date, .time {
   color: #bdc3c7;
   font-weight: 300;
   font-size: 1.5em;
   padding: 20px;
}
.date {
   border-bottom: 2px solid #eee;
}
.time {
   font-size: 3em;
}
`
);
const iframeDoc = computed(() => {
 return `
 <!DOCTYPE html>
   <html lang="en">
     <head>
       <style>
         ${valueCSS.value}
       </style>
     </head>
     <body>
       ${valueHTML.value}
       <script>${valueJS.value}<\/script>
     </body>
   </html>
`;
});
const monacoConfig = {
 theme: 'vs-dark',
 minimap: { enabled: false },
 fontSize: 16,
 scrollBeyondLastLine: false,
 automaticLayout: true,
 wordWrap: 'on',
 wordWrapColumn: 80,
 wordWrapMinified: true,
 lineNumbers: 'on',
 lineNumbersMinChars: 3,
 lineDecorationsWidth: 10,
 lineHeight: 20,
 // glyphMargin: true,
 folding: true,
 renderLineHighlight: 'all',
 renderIndentGuides: true,
 renderFinalNewline: true,
 renderLineHighlightOnlyWhenFocus: true,
 renderValidationDecorations: 'on',
 cursorBlinking: 'blink',
 cursorSmoothCaretAnimation: true,
 cursorStyle: 'line',
};
</script>
<style>
.monaco-editor-container {
 min-height: 530px;
 height: 530px;
}
.iframe-container {
 width: 100%;
 height: 530px;
}
#iframe-result {
 width: 100svw;
 min-height: 530px;
 height: 800px;
 border: none;
}
</style>
```


上記で説明した部分が今回のメインとなるロジックになります。


## **追加できなかった部分**

**JavaScriptを実行した際にconsole.logの値をブラウザ（consoleタブ）に表示**

これは、以下の理由より完成できませんでした。



* valueJS→iframe内のHTMLのみでレンダー→データがemitされないのでNuxtでデータをつかめない


自由にリポのクローンをしてください。もしconsole.log()の出し方が分かる方、是非教えてください。
お疲れ様でした。