BASEæ ªåŒäŒç€Ÿ Owners Experience Frontend ããŒã ã®ãã³ãïŒ @Panda_Program ïŒã§ãã BASE ã§ã¯ BASE ã® UI ãæ§ç¯ããããã®ç€Ÿå
ã³ã³ããŒãã³ãã©ã€ãã©ãªãBBQãã䜿ã£ãŠããã³ããšã³ãã®éçºãããŠããŸãã BBQ 㯠Vue2 + Storybook v5 ã§äœæãããŠããŸããçŸåšãããã³ããšã³ãã®æå¿ãã¡ã§ Storybook ã®ããŒãžã§ã³ãææ°ã® v6.2 ã«ãã察å¿ãããŠããŸãã ãã®èšäºã§ã¯ãVue2 + Storybook v5 ã®ã³ã³ããŒãã³ãã v6 åãã«æžãæããæ¹æ³ã玹ä»ããŸãã ãªããæ¬èšäºã§ã¯Storybook v6 èªäœã®æ©èœã®èª¬æãã main.js ã preview.js ã®æžãæ¹ãšãã£ã Storybook ã®ç°å¢æ§ç¯ã®æ¹æ³ã«ã¯è§ŠããŸããã Storybook ã³ã³ããŒãã³ãã v5 ãã v6 ã«æžãæãã ããã§ã¯ button-group ã v5 ãã v6 ã«æžãæããäŸã玹ä»ããŸãã ãŸã㯠v5ãv6 ã®æžãæ¹ãããããã芧ãã ããããã®åŸã倿Žç¹ããããã解説ãããŠãããŸãã v5 ã® button-group.stories.js // bbq/stories/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // Storybook ã³ã³ããŒãã³ãå const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories // addon-knob .addDecorator(withKnobs) // addon-info .addDecorator(withInfo) .add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // Vue Template template: ` <div : class = "'theme-'+device" > <p> <h2>ããã©ã«ã</h2> <div>ã¢ã€ã³ã³+ã©ãã«</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ã¢ã€ã³ã³</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ã©ãã«</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>ã«ã¹ã¿ã UI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ãã©ãã°ã§äžŠã³æ¿ã' } , { label: 'æ°å€ã§äžŠã³æ¿ã' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'ãªã¹ãã§äžŠã³æ¿ã' } , { icon: 'grid' , label: 'ã°ãªããã§äžŠã³æ¿ã' } , { icon: 'attentionCircle' , label: '念ã§äžŠã³æ¿ã' } , ] } , } , // methods methods: { change: action( 'change' ), } , } } , // Parameters { notes: README, } ) v6 ã® button-group.stories.js // bbq/elements/buttonGroup/button-group.stories.js import { devices } from '../../values/Devices' import ButtonGroup from './ButtonGroup' import README from './README.md' export default { // Storybook ã³ã³ããŒãã³ãå title: "V6/Elements/ButtonGroup/Vue" , // import ããã³ã³ããŒãã³ããæå® component: ButtonGroup, // parameters parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , argTypes: { // addon-knob ã® select ã§å®çŸ©ããŠãã倿° device: { options: devices, defaultValue: devices [ 0 ] , control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } , // addon-action ã§å®çŸ©ããŠãã颿° change: { action: 'changed' } } } ; const Template = (args, { argTypes } ) => ( { components: { ButtonGroup } , props: Object .keys(argTypes), template: ` <div : class = "'theme-'+device" > <div> <h2>ããã©ã«ã</h2> <div>ã¢ã€ã³ã³+ã©ãã«</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ã¢ã€ã³ã³</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ã©ãã«</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </div> <div> <h2>ã«ã¹ã¿ã UI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </div> </div> ` } ); export const Default = Template.bind( {} ) Default.args = { // Default ã³ã³ããŒãã³ãã«äžãã Props selected: 0, tag: "ul" , icons: [{ icon: 'list' } , { icon: 'grid' }] , labels: [{ label: 'ãã©ãã°ã§äžŠã³æ¿ã' } , { label: 'æ°å€ã§äžŠã³æ¿ã' }] , iconsAndLabels: [ { icon: 'list' , label: 'ãªã¹ãã§äžŠã³æ¿ã' } , { icon: 'grid' , label: 'ã°ãªããã§äžŠã³æ¿ã' } , { icon: 'attentionCircle' , label: '念ã§äžŠã³æ¿ã' } , ] , } ; ãªãã devices ã®å®çŸ©ã¯ const devices = ['pc', 'sp'] ã§ãã Storybookv6ã®å€æŽç¹ äžèšãæ°æ§ãã¡ã€ã«ã®å€æŽç¹ãæç²ããŠã³ãŒããæ¯èŒããŸãã Storybook äžã®ã³ã³ããŒãã³ãå Storybook ã§è¡šç€ºãããã³ã³ããŒãã³ãåã®å®çŸ©æ¹æ³ã®å€æŽç¹ã§ãã // v5 import { storiesOf } from '@storybook/vue' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) // v6 export default { title: "Elements/ButtonGroup" , // ... } v6 ã§ã¯ @storybook/vue ã import ããå¿
èŠããªããªããŸããããã®ä»£ããã«ãdefault export ãããªããžã§ã¯ãå
ã«ã³ã³ããŒãã³ãã®ã¡ã¿æ
å ±ãèšè¿°ããŸãã 衚瀺ããã³ã³ããŒãã³ããæå® ã³ã³ããŒãã³ããæå®ããç®æã倿Žã«ãªã£ãŠããŸãã // v5 import { storiesOf } from '@storybook/vue' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories.add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // ... } } ) // v6 import ButtonGroup from './ButtonGroup' export default { // ... component: ButtonGroup, } v6 ã§ã¯ã³ã³ããŒãã³ãã default export ãããªããžã§ã¯ãã®ããããã£ã«è¿œå ããŸãã parametersãå®çŸ©ããç®æã®å€æŽ v5 ã§ã¯ add ã¡ãœããã®ç¬¬äžåŒæ°ã ã£ã parameters ã®å®çŸ©ç®æããv6 ã§ã¯ default export ãããªããžã§ã¯ãã«å€æŽã«ãªããŸãããããã§ã¯ããŒã¯ããŠã³ãã¡ã€ã« README.md ã v6 ã§èªã¿èŸŒããæžãæ¹ã玹ä»ããŸãã // v5 import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import README from '../../elements/buttonGroup/README.md' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories .addDecorator(withInfo) // decorator ã§ addon-vue-info ãæŽ»çšããŠãã .add( 'ButtonGroup' , () => { ... } , { notes: README } ) // v6 import README from './README.md' export default { // ... parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , } v6 ã§ã¯ Storybook äžã® Docs ã¿ãã§ README.md ã衚瀺ã§ããŸãããã®ããã @storybook/addon-notes ã storybook-addon-vue-info ã¯äžèŠã«ãªããŸãã ä»å㯠v6 ã®ã³ã³ããŒãã³ãå
ã§å®çŸ©ããŸãããã preview.js ã«ä»¥äžã®ããã«èšè¿°ãããš Storybook ã®å
šã³ã³ããŒãã³ãã« parameters ã远å ãããããã docs ãåãã¡ã€ã«ã§èšè¿°ããããšé¿ããããŸãïŒ ãMigrating from notes/info addonsã ïŒã // preview.js import { addParameters } from '@storybook/client-api' ; addParameters( { docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } , } ); ãªããä»åã¯æ¢åè³ç£ã掻ããããã«ããŒã¯ããŠã³ãã¡ã€ã«ããã®ãŸãŸäœ¿ããŸãããã MDXãçšããæ¹æ³ãå
¬åŒã§ç޹ä»ãããŠããŸãã addon-knob ã®æžãæã v5 ã§ã¯ addon-knob ã䜿ããš Storybook äžã§ã³ã³ããŒãã³ãã«äžããå€ãç»é¢äžã§å€æŽã§ããŸããã v6 ã§ã¯ addon-essentials ã«å«ãŸããŠãã controls ã䜿ãã°åæ§ã®ããšãã§ããŸãã 以äžã§ã¯ knob ã® number ã select ã text 颿°ãæžãæããŠããŸãã // v5 import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { devices } from '../../values/Devices' buttonStories. add( // ... data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , props: { tag: { default : text( 'tag' , 'ul' ) } , } , } ) // v6 import { devices } from '../../values/Devices' export default { // ... argTypes: { // select 颿°ã§äœæããŠããå€ device: { options: devices, defaultValue: devices [ 0 ] , // åæå€ã®èšå® control: { type: "select" } // ãã®è¡ã¯çç¥å¯èœ } , width: { options: [ "" , "full" ] , control: { type: "select" } // ãã®è¡ã¯çç¥å¯èœ } , } ; const Template = (args, { argTypes } ) => ( { ... } ); export const Default = Template.bind( {} ) Default.args = { selected: 0, // number 颿°ã§äœæããŠããå€ tag: "ul" , // text 颿°ã§äœæããŠããå€ } ; select 颿°ã®ä»£ããã«ãªã control: { type: "select" } ã§å®çŸ©ããå€ã¯ã defaultValue ã§åæå€ãèšå®ã§ããŸãã ãªãã export default ã®äžã§å®çŸ©ããŠãã device ã width ã¯ä»¥äžã®ããã« Default.args ã§å®çŸ©ããããšãå¯èœã§ãã // v6 Default.args = { device: devices, width: [ "" , "full" ] , selected: 0, tag: "ul" , } ; ïŒåè: Dealing with complex values ïŒ addon-actions ã® action 颿°ã®æžãæã addon-actions ã䜿ã£ããããŒã®ã³ãŒã«ããã¯é¢æ°ã®å®çŸ©æ¹æ³ã倿Žã«ãªããŸããã // v5 import { action } from '@storybook/addon-actions' buttonStories .add( // ... () => { // ... methods: { change: action( 'changed' ), } , } } ) // v6 export default { argTypes: { change: { action: 'changed' } } } ; ïŒåè: addon-actions ïŒ ãã ãã以åã®ããã« action 颿°ãçšããŠãåé¡ãªãåäœãããããæžãæãã¯å¿
é ã§ã¯ãããŸããã ã³ã³ããŒãã³ãã«æž¡ãããŒã¿ã®å®çŸ©ã®å€æŽ v5 ã§èšè¿°ããŠãã data, props, computed ã§å®çŸ©ããŠããå€ã Storybook ã®ç»é¢äžã§èªç±ã«å€æŽãããå Žåã¯ã argTypes ã args ã«éçŽå¯èœã§ãã // v5 buttonStories .add( // ... () => { // ... // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ãã©ãã°ã§äžŠã³æ¿ã' } , { label: 'æ°å€ã§äžŠã³æ¿ã' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'ãªã¹ãã§äžŠã³æ¿ã' } , { icon: 'grid' , label: 'ã°ãªããã§äžŠã³æ¿ã' } , { icon: 'attentionCircle' , label: '念ã§äžŠã³æ¿ã' } , ] } , } , // ... // v6 export default { // ... argTypes: { // addon-knob ã® select ã§å®çŸ©ããŠãã倿° device: { options: devices, control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } , } ; // ... export const Default = Template.bind( {} ) Default.args = { selected: 0, tag: "ul" , icons: [{ icon: 'list' } , { icon: 'grid' }] , labels: [{ label: 'ãã©ãã°ã§äžŠã³æ¿ã' } , { label: 'æ°å€ã§äžŠã³æ¿ã' }] , iconsAndLabels: [ { icon: 'list' , label: 'ãªã¹ãã§äžŠã³æ¿ã' } , { icon: 'grid' , label: 'ã°ãªããã§äžŠã³æ¿ã' } , { icon: 'attentionCircle' , label: '念ã§äžŠã³æ¿ã' } , ] , } ; ãã ããdata ã propsãcomputed ããã®ãŸãŸæ®ãããšãå¯èœã§ãããã®å Žåã props 以å€ã¯ GUI äžã§å€ã倿Žã§ããŸãããStorybook ã® GUI äžã§å€æŽãããå€ã§ããã°ãargs ã§èšè¿°ããã°è¯ããšæããŸãã BASE BBQ ã®.Storybook ã§ã¯æ§ã
ãªãã¿ãŒã³ãããããããŸã㯠v6 ã®æžãæããåªå
ããŠããŸãããã®ãããdata çã§å®çŸ©ããŠããå€ã¯äžæŠ args ã«éçŽããã³ã³ããŒãã³ãããšã®çްãã調æŽã¯åå¥ã«å¯Ÿå¿ããäºå®ã§ãã 以äžã®ãã¿ãŒã³ã§ BASE ã® BBQ ã§äœæããã Storybook ã³ã³ããŒãã³ãã®å€§æµã®ã±ãŒã¹ãç¶²çŸ
ããŠããŸãã ãã®ä»ããã詳ãã倿Žç¹ã¯ã Storybook 6 Migration Guide ãã芧ãã ããã addon ã«ã€ã㊠v6 ã§å©çšå¯èœãª Essential addons ã«ã¯ãä»ãŸã§ã®äž»èŠãª.addon ã®æ©èœããŸãšããããŠããŸãã Docs Controls Actions Viewport Backgrounds Toolbars & globals Storybook ã§éçºããã«ãããã @storybook/addon-essentials ã¯éçºäœéšãåäžãããŠããããããv6 ããã¯å¿
é ãšãã£ãŠãéèšã§ã¯ãªãã§ãããã ããã§ã¯ãå
çšç޹ä»ããäŸã§äœ¿çšããŠãã addon ã®ã¿åãäžããŸãã addon-knob v6 ã§ã¯ deprecated addon-essentials ã® controls ã代ããã«äœ¿ã addon-info knob ãšåæ§ã« v6 ã§ã¯ deprecated addon-essentials ã® docs ã代ããã«äœ¿ã addon-action v7 ã§ deprecated ã«ãªãäºå® v6 ã§ã¯ãŸã 䜿ããããå¯èœãªã control ã«çœ®ãæãããšæ¬¡ã®ããŒãžã§ã³ã¢ãããã¹ã ãŒãºã«ãªã v6 ã§ addon-action ã䜿ãããå Žåã¯ã以äžã®ããã«èšè¿°ããã° OK ã§ãã const Template = (args, { argTypes } ) => ( { // ... template: `...`, methods: { change: action( "changed" ), } } ); 衚瀺ã確èªãã v6 ã®æžãæ¹ã«å€æŽããã³ã³ããŒãã³ããå®éã«Storybook ã§è¡šç€ºãããšä»¥äžã®ããã«ãªããŸãã Storybookã®ButtonGroup ã³ããŒãã³ã control ã§å€ã倿ŽããŠæ§ã
㪠props ã®è¡šç€ºã±ãŒã¹ã確èªã§ããããã«ãªããŸããã ãŸãããDocsããšããã¿ããã¯ãªãã¯ãããš README ã衚瀺ãããŠããŸãã Storybookã®ButtonGroupã³ã³ããŒãã³ãã®Docs ããã§ v6 ãžã®æžãæããå®äºããŸããã Storybook v6 ã§åäžããéçºäœéš v5 ãšç°ãªããv6 ã§ã¯ä»¥äžã®ç¹ã§éçºäœéšãåäžããŸããã ã³ã³ããŒãã³ãã«äžããããŒã¿ã Vue ã®å€ïŒargs, argTypesïŒã§å®çŸ©ã§ãã args ãšããŠå®çŸ©ããå€ã¯ addon-knob ã䜿ããªããŠã Storybook äžã§å€ãæžãæãããã äžèšã®äŸã§ã¯åãäžããŠããŸããããargs ãå€ããããšã§ã³ã³ããŒãã³ãã®ããªãšãŒã·ã§ã³ã容æã«äœæã§ããïŒ using-args ïŒ ã€ã³ã¹ããŒã«ãã addon ã®æ°ããstories ãã¡ã€ã«ã®ãã€ã©ãŒãã¬ãŒããæžã£ã ãããã« ä»å㯠Vue2 + Storybook v5 ã®ç°å¢ã§ Storybook ã v6 ã«ã¢ããããŒãããè©³çŽ°ãªæ¹æ³ã玹ä»ããŸããã Storybook ã®ããŒãžã§ã³ã¢ããã«ãããæ¬èšäºã®å
容ãåèã«ãªãã°å¹žãã§ãã å€ãã®æ¹ã¯ãæ°ã¥ãã ãšæããŸãããv5 ãã v6 ãžã®æžãæããšãã£ãŠããã¿ãŒã³ã決ãŸã£ãŠããŸãã ãã®ãããæé ããåãã£ãŠããŸãã°ããã°ã©ã ã§æ©æ¢°çã«çœ®æããã ãã§å¯Ÿå¿ã§ããŸãã ãã®èãæ¹ãããšã«ãTypeScript Compiler API ã䜿ã£ãŠã¡ã¿ããã°ã©ãã³ã°ã§ v5 ã®ã³ã³ããŒãã³ãã v6 ã«æžãæããã®ã§ã次åã®èšäºã§ãã®æ¹æ³ãã玹ä»ããããšæããŸãã