前置き
コンポーネントの分類の仕方についてと
少しですがサンプルコード集です🎈🧸
この要素はどこ❓
molecules❓
それともorganisms❓
なんて時にチェックしてください✅🌟
フォルダ分けはアトミックデザインを推奨しています。
Atomic Designとは
サンプルコードもあるので
コンポーネントの命名や
中身の構成などの参考にも
お役立てください❤️
コードを書き始める前のチェックリストを
見ておくと尚良いと思います✨👀
分け方
アトミックデザインに基づく分け方
アトミックデザイン は、
要素の大きさや機能ごとに
ファイルを分けるやり方です✨🗂
それに乗っ取って分けていますが、
確実な正解・ルールは存在しないので
あくまでも私たちの分け方として
参考にしてくださいね♪
それぞれ、
どの階層のコンポーネントを
読み込んでもOKです🙆♀️
atoms
UIの最小要素。button
, icon
, input
など。
タイトルのh1
と、
サブタイトルのp
でセットの場合なんかも
1つのまとまりなのでatomsへ🍎
molecules
atomsを2, 3つ貼り付けたような物🧩ul > li
のli
やform
に入れるlabel
つきのinput
など。
organismがform
やul
などの
まとまりなので
それを分解した要素です💡
InputDefault.vueを
atomで作っていた場合はimportし、
作っていない場合は
FormItemInput.vueで
直接inputを使ってもOKです⭕️
organisms
form
やul
など、
ある程度のまとまり。
modalの中身もココ💫
templates
modalやnav
, Header
, Footer
section
などの大きなまとまり🍓
atoms
buttons
ButtonDefault.vue
ボタンをコンポーネントにする場合は
必ず$emit
が必要になりますね💡
https://wp.me/pc9NHC-od
テキストは親によって
変わることがほとんどなので
slotを使用しています🙋♀️
https://wp.me/pc9NHC-k3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<template> <button class="button button-default" @click="$emit('click', $event)" > <slot name="label"> LABEL </slot> </button> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({}) </script> <style lang="scss" scoped> .button-default { } </style> |
入力項目を全て入力しないと
ボタンが押せないようにしたい場合。props
のstatusと:class
の
クラスバインディングを付け足します😉
scssは親でstatusが
inactiveになった時の
スタイリングを記載しています✍️
リンク先はv-bind="$attrs"
が便利です。
https://wp.me/pc9NHC-li
色はscssの変数を使っています❗️🎨
楽ちんなのでflex派ですが、
時々バグって最後の1文字だけ
改行されることがあるようです、、、
その場合はpadding
, line-height
, text-aline
で調整しましょう!
https://wp.me/pc9NHC-sc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
<template> <button v-bind="$attrs" :class="classes" :disabled="status === 'disable'" type="button" class="button button-default" @click="$emit('click')" > <span class="label"> <slot>LABEL</slot> </span> </button> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'ButtonDefault', props: { status: { type: String, default: '', required: false, validator (value) { return [ '', 'inactive', ].includes(value) }, }, }, computed: { classes () { switch (this.status) { case '': return '' case 'inactive': return 'inactive' default: return '' } }, }, }) </script> <style lang="scss" scoped> .button-default { display: flex; justify-content: center; align-items: center; width: 100%; min-height: 48px; border: none; border-radius: 1px; appearance: none; &:hover { background-color: $color-ui; } > .label { @include font-button; text-align: center; letter-spacing: 0.08em; color: $color-ui-white; } &.inactive { background-color: $color-ui-gray; &:hover { background-color: $color-ui-gray; } } } </style> |
icons
よく使うアイコンなどは
予めコンポーネントにしちゃいましょう❣️
クリックイベントがあったり、
親によってつけるクラスを変えたい場合は
⬆️のbuttonを参考にしてください🌟
IconEdit.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<template> <div class="icon icon-edit" @click="$emit('clickEdit')" > <svg class="icon" viewBox="0 0 24 24" > <use xlink:href="#edit" /> </svg> </div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'IconEdit', }) </script> <style lang="scss" scoped> .icon-edit { display: flex; justify-content: center; align-items: center; background-color: $color-ui; border-radius: 50%; width: 24px; min-width: 24px; height: 24px; > .icon { fill: $color-ui-white; width: 10px; height: 10px; } } </style> |
title
メインタイトルのみでも⭕️
サブタイトルもセットの場合も
1つのタイトルのまとまりなのでatom🌟
TitlePage.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<template> <div class="title title-page"> <h2 class="title"> <slot name="title"> Title </slot> </h2> <p class="sub-title"> <slot name="subtitle"> Sub Title </slot> </p> </div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'TitlePage', }) </script> <style lang="scss" scoped> .title-page { text-align: center; .sub-title { color: $color-ui-sub; } } </style> |
molecules
FormItemInput
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
<template> <label :class="classes" class="form-item form-item-input" > <span class="label"> {{ label }} </span> <input v-bind="$attrs" :type="type" :placeholder="placeholder" class="input" @input="$emit('input', $event.target.value)" > </label> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'FormItemInput', props: { status: { type: String, default: '', required: false, validator (value) { return [ '', 'inactive', ].includes(value) }, }, label: { type: String, default: 'label', }, type: { type: String, default: 'text', }, placeholder: String, }, computed: { classes () { switch (this.status) { case '': return '' case 'inactive': return 'inactive' default: return '' } }, }, }) </script> <style lang="scss" scoped> .form-item-input { display: flex; align-items: center; > .label { width: 64px; min-width: 64px; } > .input { width: calc(100% - 64px); padding: 8px 12px; border: 2px solid $color-ui; &::placeholder { color: $color-font-gray; } } &.inactive { > .label { color: $color-ui-sub; } > .input { border: 2px solid $color-ui-sub; } } } </style> |
FormItemTextarea
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
<template> <label :class="classes" class="form-item form-item-textarea" > <span class="label"> {{ label }} </span> <div v-bind="$attrs" class="textarea" contenteditable="true" role="textbox" @input="$emit('input', $event.target.textContent)" /> </label> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'FormItemTextarea', props: { status: { type: String, default: '', required: false, validator (value) { return [ '', 'inactive', ].includes(value) }, }, label: { type: String, default: 'label', }, placeholder: String, }, computed: { classes () { switch (this.status) { case '': return '' case 'inactive': return 'inactive' default: return '' } }, }, }) </script> <style lang="scss" scoped> .form-item-textarea { display: flex; align-items: flex-start; > .label { width: 64px; min-width: 64px; padding-top: 11px; } > .textarea { width: calc(100% - 64px); min-height: 120px; padding: 8px 12px; border: 2px solid $color-ui; color: $color-font; } &.inactive { > .label { color: $color-ui-sub; } > .textarea { border: 2px solid $color-ui-sub; } } } </style> |
ListItemEvent
イベントを作るアプリの体です。
イベントが出来たらli
で表示させています。
atomsをimportしています。
親で写真やユーザー情報を入れられるようにprops
にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
<template> <nuxt-link v-bind="$attrs" class="list-item list-item-event" tag="li" > <a class="link"> <ImageEventThumb :image="event.thumbnail" /> <div class="info"> <IconUser :image="event.owner.image" status="small" /> <div class="info"> <p class="title"> {{ event.title }} </p> <div class="info"> <p class="address"> {{ event.address }} </p> <p class="date"> at {{ $dayjs(event.date).format('YYYY.MM.DD') }} </p> </div> </div> </div> </a> </nuxt-link> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'ListItemEvent', components: { ImageEventThumb: () => import('@/components/atom/images/ImageEventThumb.vue'), IconUser: () => import('@/components/atom/icons/IconUser.vue'), }, props: { event: { type: Object, required: true, }, }, }) </script> <style lang="scss" scoped> .list-item-event { > .link { display: block; > .image { margin-bottom: 8px; } > .info { display: flex; align-items: flex-start; padding: 0 20px; > .icon { margin-top: 2px; margin-right: 8px; } > .info { width: calc(100% - 48px); > .title { @include font-text; word-wrap: break-word; } > .info { display: flex; > .address, > .date { @include font-text-small; color: $color-font-gray; white-space: nowrap; } > .address { max-width: 100%; overflow: hidden; text-overflow: ellipsis; } > .date { margin-left: 8px; } } } } } } </style> |
organisms
FormLogin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<template> <form class="form form-login"> <FormItemInputColumn status="inactive" label="MAIL" placeholder="your@email.address" /> <FormItemInputColumn status="inactive" label="PASSWORD" placeholder="Password" type="password" /> <div class="buttons"> <ButtonDefault> START </ButtonDefault> <ButtonDefault status="inactive" label="ATTEND" > CANCEL </ButtonDefault> </div> </form> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'FormLogin', components: { FormItemInputColumn: () => import('@/components/molecule/formItems/FormItemInputColumn.vue'), ButtonDefault: () => import('@/components/atom/buttons/ButtonDefault.vue'), }, }) </script> <style lang="scss" scoped> .form-login { > .form-item { margin-top: 8px; &:first-child { margin-top: 0; } } > .buttons { margin-top: 24px; > .button { margin-top: 8px; &:first-child { margin-top: 0; } } } } </style> |
ListEvent
ListItemEventを並べるだけ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<template> <ul class="list list-event"> <ListItemEvent v-for="(event, index) in events" :key="index" :event="event" to="/" /> </ul> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'ListEvent', components: { ListItemEvent: () => import('@/components/molecule/listItems/ListItemEvent.vue'), }, props: { events: { type: Array, required: true, }, }, }) </script> <style lang="scss" scoped> .list-event { > .list-item { margin-top: 24px; &:first-child { margin-top: 0; } } } </style> |
templates
Modal
モーダルの中身は親で
organismを指定したいのでslot
にしてあります🍓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<template> <div class="modal modal-sp"> <div class="container"> <slot /> </div> </div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'ModalSp', }) </script> <style lang="scss" scoped> .modal-sp { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 100%; max-width: 375px; border-radius: 24px 24px 0 0; background-color: $color-bg; > .container { padding: 40px 20px 56px; } } </style> |
親で中身を指定☝️🌟
1 2 3 4 5 6 7 8 9 |
<template> <div class="page page-my-page"> <Modal class="modal" > <FormIcon /> </Modal> </div> </template> |
SectionEvent
ListEventをimportしています。
イベントのジャンルなどをlabel
に記載、
+ボタンでイベントの追加ができたり
MOREでもっとイベントを見ることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
<template> <div :class="classes" class="section section-event" > <label class="label"> <span v-if="label" class="label" > {{ label }} </span> <IconPlus v-if="status === 'add'" /> </label> <ListEvent :events="events" /> <div v-if="hasMore" class="more" > <nuxt-link v-bind="$attrs" class="link" @click.native="isShow = true" > <span> MORE </span> </nuxt-link> </div> </div> </template> <script lang="ts"> import Vue from 'vue' export default Vue.extend({ name: 'SectionEvent', components: { IconPlus: () => import('@/components/atom/icons/IconPlus.vue'), ListEvent: () => import('@/components/organism/lists/ListEvent.vue'), }, props: { status: { type: String, default: '', required: false, validator (value) { return [ '', 'add', ].includes(value) }, }, events: { type: Array, required: true, }, label: { type: String, required: false, }, hasMore: { type: Boolean, required: false, }, }, computed: { classes () { switch (this.status) { case '': return '' case 'add': return 'add' default: return '' } }, }, }) </script> <style lang="scss" scoped> .section-event { > .label { display: flex; justify-content: space-between; align-items: center; margin: 0 20px; } > .list { margin-top: 8px; } > .more { display: flex; justify-content: flex-end; > .link { @include font-text-small; position: relative; color: $color-ui; padding: 4px 20px; text-decoration: underline; } } &.add { > .list { margin-top: 4px; } } } </style> |
まとめ
アトミックデザインでの分類は
管理がしやすくなって便利ですね❤️
命名やコードもある程度きめて
テンプレ化しておけば
他の開発でも使い回しが効くので
よりスムーズな開発が進められると思います✨
❗️とはいえ丸コピはミス多発する原因なので(特にTS)
あくまで見ながら構成を真似するようにしましょう…