├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── slides ├── SLIDES.md ├── css │ ├── custom.css │ ├── reveal.css │ └── theme │ │ └── black.css ├── index.tpl.html ├── js │ ├── reveal.js │ └── reveal.min.js ├── lib │ ├── css │ │ └── zenburn.css │ └── js │ │ ├── classList.js │ │ ├── head.min.js │ │ └── html5shiv.js ├── md2reveal.js ├── package.json └── plugin │ └── highlight │ └── highlight.js ├── tasks ├── 1 │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ ├── Hero.vue │ │ │ └── HeroList.vue │ │ └── main.js │ └── tests │ │ └── unit │ │ ├── .eslintrc.js │ │ └── Hero.spec.js ├── 2 │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.vue │ │ ├── components │ │ ├── APIFetchMixin.js │ │ ├── PeopleList.vue │ │ └── PlanetList.vue │ │ └── main.js ├── 3 │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ └── SelectOther.vue │ │ └── main.js │ └── tests │ │ └── unit │ │ ├── .eslintrc.js │ │ └── SelectOther.spec.js ├── 4 │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ └── Modal.vue │ │ └── main.js │ └── tests │ │ └── unit │ │ ├── .eslintrc.js │ │ └── Modal.spec.js ├── 5 │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ └── PageLayout.vue │ │ ├── main.js │ │ ├── pages │ │ │ ├── Home.vue │ │ │ ├── Page1.vue │ │ │ ├── Page2.vue │ │ │ └── Page3.vue │ │ └── router │ │ │ └── index.js │ └── tests │ │ └── unit │ │ ├── .eslintrc.js │ │ └── PageLayout.spec.js ├── 6 │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.vue │ │ ├── components │ │ ├── Hero.vue │ │ └── HeroList.vue │ │ └── main.js ├── 7 │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ ├── Square.vue │ │ │ └── ThemeProvider.vue │ │ └── main.js │ └── tests │ │ └── unit │ │ ├── .eslintrc.js │ │ └── App.spec.js ├── 8 │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── App.vue │ │ ├── components │ │ └── MultiSelect.vue │ │ └── main.js ├── 9 │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── components │ │ │ ├── Tab.vue │ │ │ └── Tabs.vue │ │ └── main.js │ └── tests │ │ └── unit │ │ ├── .eslintrc.js │ │ └── Tabs.spec.js └── tpl │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ └── src │ ├── App.vue │ └── main.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/prettier'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | }, 11 | parserOptions: { 12 | parser: 'babel-eslint', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | #.vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | slides/index.html 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "vetur.validation.template": false, 4 | "eslint.validate": [ 5 | "javascript", 6 | "javascriptreact", 7 | { 8 | "language": "vue", 9 | "autoFix": true 10 | } 11 | ], 12 | "vetur.format.defaultFormatter.html": "none", 13 | "eslint.autoFixOnSave": true 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Component Patterns in Vue.js Workshop 2 | 3 | ### Project setup 4 | 5 | ``` 6 | yarn 7 | ``` 8 | 9 | > Note: this repository only works with `yarn` because it uses [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). If you don't have it, install it with `npm -g install yarn`. 10 | 11 | ### Run a task 12 | 13 | ``` 14 | yarn task1 15 | ``` 16 | 17 | ### Task list 18 | 19 | 1. [Warm-up — props & events](tasks/1/README.md) 20 | 2. [Extracting common logic from components — mixins](tasks/2/README.md) 21 | 3. [Components with `v-model`](tasks/3/README.md) 22 | 4. [`Modal` component — slots](tasks/4/README.md) 23 | 5. [`PageLayout` component — named slots & lazy-loading](tasks/5/README.md) 24 | 6. [Customizing children UI — scoped slots](tasks/6/README.md) 25 | 7. [`ThemeProvider` component — provide / inject](tasks/7/README.md) 26 | 8. [Wrapping an [insert library here] component](tasks/8/README.md) 27 | 9. [`Tabs` & `Tab` components](tasks/9/README.md) 28 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-advanced-components-workshop", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "task1": "yarn workspace task1 serve", 7 | "task1:test": "yarn workspace task1 test:unit", 8 | "task2": "yarn workspace task2 serve", 9 | "task3": "yarn workspace task3 serve", 10 | "task3:test": "yarn workspace task3 test:unit", 11 | "task4": "yarn workspace task4 serve", 12 | "task4:test": "yarn workspace task4 test:unit", 13 | "task5": "yarn workspace task5 serve", 14 | "task5-serve": "yarn workspace task5 build && yarn serve -s tasks/5/dist", 15 | "task5:test": "yarn workspace task5 test:unit", 16 | "task6": "yarn workspace task6 serve", 17 | "task7": "yarn workspace task7 serve", 18 | "task7:test": "yarn workspace task7 test:unit", 19 | "task8": "yarn workspace task8 serve", 20 | "task9": "yarn workspace task9 serve", 21 | "task9:test": "yarn workspace task9 test:unit", 22 | "slides": "yarn workspace slides serve" 23 | }, 24 | "workspaces": [ 25 | "tasks/*", 26 | "slides" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /slides/SLIDES.md: -------------------------------------------------------------------------------- 1 | ## Agenda 2 | 3 | - (very) short intro to components 4 | - ~9 tasks 5 | - present the concept 6 | - describe the task 7 | - 10 minutes for you to solve the task ⇄ 5 more minutes to solve the task together 8 | - (if there's time left) one freestyle task 9 | - 15' coffee break 10 | 11 | ## Components 12 | 13 | - What is an (UI) component? 14 | - & "a visual entity that encapsulates a piece of functionality" 15 | - & a custom DOM element 16 | - & "smart" vs. "dumb" / stateful vs. stateless / [presentational vs. container](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 17 | 18 | ## Components in Vue.js 19 | 20 | - component object definition 21 | 22 | ```js 23 | export default { 24 | name: 'component', 25 | props: { 26 | /* ... */ 27 | }, 28 | data() { 29 | return { 30 | /* ... */ 31 | }; 32 | }, 33 | methods: { 34 | /* ... */ 35 | }, 36 | /* ... */ 37 | }; 38 | ``` 39 | 40 | - single file components `Component.vue` 41 | - component hierarchy 42 | - it's a tree 43 | - it has a main root instance 44 | - components have a `$parent` and zero or more `$children` 45 | 46 | ## Vue.js Components Internal API 47 | 48 | - data() 49 | - computed 50 | - methods 51 | - lifecycle hooks 52 | - watch 53 | - refs 54 | - render 55 | - mixins 56 | - $el, $parent, \$children [etc.](https://vuejs.org/v2/api/) 57 | 58 | ## Vue.js Components External API 59 | 60 | - props 61 | - events 62 | - **slots** 63 | - provide / inject 64 | 65 | ## Tasks 66 | 67 | 1. Warm-up — props & events 68 | 2. Extracting common logic from components — mixins 69 | 3. Components with `v-model` 70 | 4. `Modal` component — slots 71 | 5. `PageLayout` component — name slots & lazy-loading 72 | 6. Customizing children UI — scoped slots 73 | 7. `ThemeProvider` component — provide / inject 74 | 8. Wrapping an [insert library here] component 75 | 9. `Tabs` & `Tab` components 76 | 10. ??? 77 | 78 | ## Tests 79 | 80 | - run with `yarn task#:test [--watch]` 81 | - using `jest` and `@vue/test-utils` 82 | - using `jsdom` to "emulate" DOM, might be different than browsers 83 | 84 | ## Warming Up - Task 1 85 | 86 | Create a component that receives props and sends events. 87 | 88 | - run with `yarn task1` 89 | 90 | ## Mixins 91 | 92 | - a way to extract common functionality out of components 93 | - a definition object, exactly like with components 94 | - when a component uses a mixin, its definition properties are merged with those of the mixin 95 | 96 | ## Task 2 97 | 98 | Extract the common API fetch logic from two components. 99 | 100 | ## Mixins Pros & Cons 101 | 102 | - pros 103 | - DRY 104 | - reusability 105 | - cons 106 | - hidden (implicit) dependencies — documentation 107 | - name clashes — "namespacing" 108 | - [Mixins Considered Harmful](https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html) 109 | 110 | ## Components and `v-model` 111 | 112 | ```html 113 | 114 | ``` 115 | 116 | is syntactic sugar for 117 | 118 | ```html 119 | 120 | ``` 121 | 122 | ## Task 3 123 | 124 | Create a component for an extended select that allows for an "Other" option and works with `v-model`. 125 | 126 | ## Slots 127 | 128 | - allow components to send markup to their children 129 | - change the render tree 130 | 131 | ## Default slot 132 | 133 | - in parent 134 | 135 | ```html 136 | 137 |
138 | some markup here 139 |
140 |
141 | ``` 142 | 143 | - in child 144 | 145 | ```html 146 | 149 | ``` 150 | 151 | - default content 152 | 153 | ```html 154 | 155 |
156 | some default content here 157 |
158 |
159 | ``` 160 | 161 | ## Task 4 162 | 163 | Create a modal component. 164 | 165 | ## Named Slots 166 | 167 | - new syntax since 2.6 168 | 169 | - in parent 170 | 171 | ```html 172 | 173 |
174 | some markup here 175 |
176 |
177 | ``` 178 | 179 | - in child 180 | 181 | ```html 182 | 187 | ``` 188 | 189 | ## Task 5 190 | 191 | Create a `PageLayout` component that takes 3 slots — default, `header` and `footer`. 192 | 193 | ## PageLayout Note 194 | 195 | - navigating between pages repaints the whole layout (not really noticeable, though) 196 | 197 | ## Lazy-loaded Components 198 | 199 | - don't load **all** of your application upfront if you don't really need to 200 | - usually used with route components 201 | 202 | ```js 203 | const Page = () => import('./path/to/Page.vue'); 204 | ``` 205 | 206 | - that's pretty much it! 🙂 (thanks to webpack's magic) 207 | 208 | ## Task 5 - take 2 209 | 210 | Make the 3 pages from task 5 lazy-loaded. 211 | 212 | ## Scoped Slots 213 | 214 | - slots with parameters 215 | - in parent 216 | 217 | ```html 218 | 221 | ``` 222 | 223 | - in child 224 | 225 | ```html 226 | 227 | default slot contents here 228 | 229 | ``` 230 | 231 | ## Task 6 232 | 233 | Use scoped slots to customize how a `Hero` row from task 1 looks like. 234 | 235 | ## Provide / Inject 236 | 237 | - allows an up the tree component to send (provide) data to all its descendants 238 | - in the provider: 239 | 240 | ```js 241 | export default { 242 | /* ... */ 243 | provide() { 244 | return { 245 | theme: { 246 | color: 'orangered', 247 | /* ... */ 248 | }, 249 | }; 250 | }, 251 | }; 252 | ``` 253 | 254 | - in a descendant: 255 | 256 | ```js 257 | export default { 258 | /* ... */ 259 | inject: ['theme'], // accessible as this.theme 260 | }; 261 | ``` 262 | 263 | - the provided object is not reactive (but we can circumvent that) 264 | 265 | ## Task 7 266 | 267 | Define a `ThemeProvider` that provides a `theme` object, with `color` and `backgroundColor` properties. Use the provided `theme` in a descendant component. 268 | 269 | ## Provide / Inject Pros & Cons 270 | 271 | - pros 272 | - send data to descendants more than one level deep 273 | - cons 274 | - not obviously reactive 275 | 276 | ## Wrapping jQuery Components 277 | 278 | - initialization goes in `mounted()` 279 | - use `this.$el`, the component's root DOM element 280 | - (two-way) react to changes during the component's lifecycle 281 | - **cleanup goes in `beforeDestroy()`** 282 | 283 | ## Task 8 284 | 285 | Refactor a `MultiSelect` component to use [select2](https://select2.org/). 286 | 287 | ## Task 9 — and the last one! 🎉 288 | 289 | Implement `Tabs` and `Tab` components. 290 | 291 | ## Resources 292 | 293 | - Vue.js documentation, pretty much 294 | - Vue.js source code 295 | - [Renderless Components in Vue.js - Adam Wathan](https://adamwathan.me/renderless-components-in-vuejs/) 296 | - [7 Secret Patterns Vue Consultants Don’t Want You to Know - Chris Fritz](https://www.youtube.com/watch?v=7lpemgMhi0k) 297 | 298 | ## Thank you! 🙏 299 | 300 | Questions? 301 | -------------------------------------------------------------------------------- /slides/css/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fefae8 !important; 3 | } 4 | 5 | .reveal h2, 6 | .reveal h3, 7 | .reveal p, 8 | .reveal ul, 9 | .reveal ol { 10 | font-family: 'Open Sans', sans-serif !important; 11 | color: #272c3c !important; 12 | } 13 | 14 | .reveal h2 { 15 | font-size: 0.9em !important; 16 | text-transform: none; 17 | } 18 | 19 | .reveal h3 { 20 | font-size: 0.7em !important; 21 | text-transform: none; 22 | } 23 | 24 | .reveal p { 25 | font-size: 0.7em !important; 26 | } 27 | 28 | .reveal pre code { 29 | font-size: 0.9em !important; 30 | line-height: 1.1em !important; 31 | max-height: 800px; 32 | padding: 10px; 33 | } 34 | 35 | .reveal ul, 36 | .reveal ol { 37 | align-self: flex-start; 38 | font-size: 0.65em !important; 39 | margin-left: 80px; 40 | } 41 | 42 | .reveal ul ul li { 43 | font-size: 1.4em !important; 44 | } 45 | 46 | .reveal ul ul { 47 | list-style-type: circle !important; 48 | } 49 | 50 | .reveal .title h2, 51 | .reveal .title p { 52 | color: #fff !important; 53 | } 54 | 55 | img.logo { 56 | background: none !important; 57 | border: none !important; 58 | box-shadow: none !important; 59 | position: absolute; 60 | left: 50px; 61 | top: 0; 62 | } 63 | 64 | .reveal .slides section { 65 | display: flex !important; 66 | align-items: center; 67 | flex-direction: column; 68 | justify-content: center; 69 | padding: 0 50px; 70 | min-height: 95% !important; 71 | width: 90%; 72 | } 73 | 74 | .slide-logo { 75 | color: #272c3c; 76 | display: flex; 77 | align-items: center; 78 | font-family: 'Open Sans', sans-serif !important; 79 | font-size: 0.4em !important; 80 | font-weight: bold !important; 81 | position: absolute; 82 | left: 50px; 83 | top: 0; 84 | } 85 | 86 | .slide-logo img { 87 | background: none !important; 88 | border: none !important; 89 | box-shadow: none !important; 90 | width: 50px; 91 | } 92 | 93 | .slide-logo div { 94 | margin-left: 10px; 95 | } 96 | 97 | img.simple { 98 | background: none !important; 99 | border: none !important; 100 | box-shadow: none !important; 101 | } 102 | 103 | a { 104 | color: #42b983 !important; 105 | } 106 | 107 | .muted { 108 | color: gray; 109 | font-weight: normal !important; 110 | } 111 | 112 | .reveal code.simple { 113 | background-color: #ff9; 114 | padding: 3px; 115 | } 116 | -------------------------------------------------------------------------------- /slides/css/theme/black.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Black theme for reveal.js. This is the opposite of the 'white' theme. 3 | * 4 | * By Hakim El Hattab, http://hakim.se 5 | */ 6 | /* @import url(../../lib/font/source-sans-pro/source-sans-pro.css); */ 7 | @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,400italic,600italic&subset=latin,latin-ext); 8 | 9 | section.has-light-background, section.has-light-background h1, section.has-light-background h2, section.has-light-background h3, section.has-light-background h4, section.has-light-background h5, section.has-light-background h6 { 10 | color: #222; } 11 | 12 | /********************************************* 13 | * GLOBAL STYLES 14 | *********************************************/ 15 | body { 16 | background: #222; 17 | background-color: #222; } 18 | 19 | .reveal { 20 | font-family: "Source Sans Pro", Helvetica, sans-serif; 21 | font-size: 42px; 22 | font-weight: normal; 23 | color: #fff; } 24 | 25 | ::selection { 26 | color: #fff; 27 | background: #bee4fd; 28 | text-shadow: none; } 29 | 30 | ::-moz-selection { 31 | color: #fff; 32 | background: #bee4fd; 33 | text-shadow: none; } 34 | 35 | .reveal .slides section, 36 | .reveal .slides section > section { 37 | line-height: 1.3; 38 | font-weight: inherit; } 39 | 40 | /********************************************* 41 | * HEADERS 42 | *********************************************/ 43 | .reveal h1, 44 | .reveal h2, 45 | .reveal h3, 46 | .reveal h4, 47 | .reveal h5, 48 | .reveal h6 { 49 | margin: 0 0 20px 0; 50 | color: #fff; 51 | font-family: "Source Sans Pro", Helvetica, sans-serif; 52 | font-weight: 600; 53 | line-height: 1.2; 54 | letter-spacing: normal; 55 | text-transform: uppercase; 56 | text-shadow: none; 57 | word-wrap: break-word; } 58 | 59 | .reveal h1 { 60 | font-size: 2.5em; } 61 | 62 | .reveal h2 { 63 | font-size: 1.6em; } 64 | 65 | .reveal h3 { 66 | font-size: 1.3em; } 67 | 68 | .reveal h4 { 69 | font-size: 1em; } 70 | 71 | .reveal h1 { 72 | text-shadow: none; } 73 | 74 | /********************************************* 75 | * OTHER 76 | *********************************************/ 77 | .reveal p { 78 | margin: 20px 0; 79 | line-height: 1.3; } 80 | 81 | /* Ensure certain elements are never larger than the slide itself */ 82 | .reveal img, 83 | .reveal video, 84 | .reveal iframe { 85 | max-width: 95%; 86 | max-height: 95%; } 87 | 88 | .reveal strong, 89 | .reveal b { 90 | font-weight: bold; } 91 | 92 | .reveal em { 93 | font-style: italic; } 94 | 95 | .reveal ol, 96 | .reveal dl, 97 | .reveal ul { 98 | display: inline-block; 99 | text-align: left; 100 | margin: 0 0 0 1em; } 101 | 102 | .reveal ol { 103 | list-style-type: decimal; } 104 | 105 | .reveal ul { 106 | list-style-type: disc; } 107 | 108 | .reveal ul ul { 109 | list-style-type: square; } 110 | 111 | .reveal ul ul ul { 112 | list-style-type: circle; } 113 | 114 | .reveal ul ul, 115 | .reveal ul ol, 116 | .reveal ol ol, 117 | .reveal ol ul { 118 | display: block; 119 | margin-left: 40px; } 120 | 121 | .reveal dt { 122 | font-weight: bold; } 123 | 124 | .reveal dd { 125 | margin-left: 40px; } 126 | 127 | .reveal blockquote { 128 | display: block; 129 | position: relative; 130 | width: 70%; 131 | margin: 20px auto; 132 | padding: 5px; 133 | font-style: italic; 134 | background: rgba(255, 255, 255, 0.05); 135 | box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } 136 | 137 | .reveal blockquote p:first-child, 138 | .reveal blockquote p:last-child { 139 | display: inline-block; } 140 | 141 | .reveal q { 142 | font-style: italic; } 143 | 144 | .reveal pre { 145 | display: block; 146 | position: relative; 147 | width: 90%; 148 | margin: 20px auto; 149 | text-align: left; 150 | font-size: 0.55em; 151 | font-family: monospace; 152 | line-height: 1.2em; 153 | word-wrap: break-word; 154 | box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } 155 | 156 | .reveal code { 157 | font-family: monospace; 158 | text-transform: none; } 159 | 160 | .reveal pre code { 161 | display: block; 162 | padding: 5px; 163 | overflow: auto; 164 | max-height: 400px; 165 | word-wrap: normal; } 166 | 167 | .reveal table { 168 | margin: auto; 169 | border-collapse: collapse; 170 | border-spacing: 0; } 171 | 172 | .reveal table th { 173 | font-weight: bold; } 174 | 175 | .reveal table th, 176 | .reveal table td { 177 | text-align: left; 178 | padding: 0.2em 0.5em 0.2em 0.5em; 179 | border-bottom: 1px solid; } 180 | 181 | .reveal table th[align="center"], 182 | .reveal table td[align="center"] { 183 | text-align: center; } 184 | 185 | .reveal table th[align="right"], 186 | .reveal table td[align="right"] { 187 | text-align: right; } 188 | 189 | .reveal table tbody tr:last-child th, 190 | .reveal table tbody tr:last-child td { 191 | border-bottom: none; } 192 | 193 | .reveal sup { 194 | vertical-align: super; 195 | font-size: smaller; } 196 | 197 | .reveal sub { 198 | vertical-align: sub; 199 | font-size: smaller; } 200 | 201 | .reveal small { 202 | display: inline-block; 203 | font-size: 0.6em; 204 | line-height: 1.2em; 205 | vertical-align: top; } 206 | 207 | .reveal small * { 208 | vertical-align: top; } 209 | 210 | /********************************************* 211 | * LINKS 212 | *********************************************/ 213 | .reveal a { 214 | color: #42affa; 215 | text-decoration: none; 216 | -webkit-transition: color .15s ease; 217 | -moz-transition: color .15s ease; 218 | transition: color .15s ease; } 219 | 220 | .reveal a:hover { 221 | color: #8dcffc; 222 | text-shadow: none; 223 | border: none; } 224 | 225 | .reveal .roll span:after { 226 | color: #fff; 227 | background: #068de9; } 228 | 229 | /********************************************* 230 | * IMAGES 231 | *********************************************/ 232 | .reveal section img { 233 | margin: 15px 0px; 234 | background: rgba(255, 255, 255, 0.12); 235 | border: 4px solid #fff; 236 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } 237 | 238 | .reveal section img.plain { 239 | border: 0; 240 | box-shadow: none; } 241 | 242 | .reveal a img { 243 | -webkit-transition: all .15s linear; 244 | -moz-transition: all .15s linear; 245 | transition: all .15s linear; } 246 | 247 | .reveal a:hover img { 248 | background: rgba(255, 255, 255, 0.2); 249 | border-color: #42affa; 250 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } 251 | 252 | /********************************************* 253 | * NAVIGATION CONTROLS 254 | *********************************************/ 255 | .reveal .controls { 256 | color: #42affa; } 257 | 258 | /********************************************* 259 | * PROGRESS BAR 260 | *********************************************/ 261 | .reveal .progress { 262 | background: rgba(0, 0, 0, 0.2); 263 | color: #42affa; } 264 | 265 | .reveal .progress span { 266 | -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 267 | -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 268 | transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } 269 | 270 | /********************************************* 271 | * PRINT BACKGROUND 272 | *********************************************/ 273 | @media print { 274 | .backgrounds { 275 | background-color: #222; } } 276 | -------------------------------------------------------------------------------- /slides/index.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | reveal.js – The HTML Presentation Framework 7 | 8 | 12 | 13 | 14 | 15 | 19 | 20 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 |
53 | 58 | 63 | 69 | 70 | 71 | Vue.js Logo 77 |

78 | Advanced Vue.js Component Patterns 79 | 80 |

Bogdan Luca 81 |
guest host Dobromir Hristov 82 |

83 |

86 | April 10th, 2019
JSHeroes 87 |

88 |
89 | 90 |
91 | 98 |

About Me, About You...

99 |

Hello, I'm Bogdan!

101 |
    102 |
  • 103 | been programming for ~25y 104 |
  • 105 |
  • 106 | sysadmin, networking, backend, frontend, ... 107 |
  • 108 |
  • 109 | dev&ops @CodeSandbox.io 112 | — "The online code editor for Web" 113 |
  • 114 |
  • 115 | (Vue.js) Instructor at JSLeague 116 |
  • 117 |
  • 118 | (used to) 24/7 support on 119 | Vue.js Official Chat 122 | — BogdanL 123 |
  • 124 |
125 |
126 |

Hello, I'm Dobri!

127 |
    128 |
  • organizer of Vue Bulgaria
  • 129 |
  • Vue.js instructor
  • 130 |
  • creator of Vue Community
  • 131 |
132 |
133 | 134 | ${CONTENT} 135 |
136 |
137 | 138 | 139 | 140 | 141 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /slides/js/reveal.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * reveal.js 3.7.0 (2019-01-25, 14:22) 3 | * http://revealjs.com 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2018 Hakim El Hattab, http://hakim.se 7 | */ 8 | 9 | !function(e,t){"function"==typeof define&&define.amd?define(function(){return e.Reveal=t(),e.Reveal}):"object"==typeof exports?module.exports=t():e.Reveal=t()}(this,function(){"use strict";var u,h,v,g,m,l,p,f,a,s=".slides section",y=".slides>section",b=".slides>section.present>section",w=".slides>section:first-of-type",k=navigator.userAgent,A={width:960,height:700,margin:.04,minScale:.2,maxScale:2,controls:!0,controlsTutorial:!0,controlsLayout:"bottom-right",controlsBackArrows:"faded",progress:!0,slideNumber:!1,hashOneBasedIndex:!1,showSlideNumber:"all",history:!1,keyboard:!0,keyboardCondition:null,overview:!0,disableLayout:!1,center:!0,touch:!0,loop:!1,rtl:!1,shuffle:!1,fragments:!0,fragmentInURL:!1,embedded:!1,help:!0,pause:!0,showNotes:!1,autoPlayMedia:null,autoSlide:0,autoSlideStoppable:!0,autoSlideMethod:null,defaultTiming:null,mouseWheel:!1,rollingLinks:!1,hideAddressBar:!0,previewLinks:!1,postMessage:!0,postMessageEvents:!1,focusBodyOnPageVisibilityChange:!0,transition:"slide",transitionSpeed:"default",backgroundTransition:"fade",parallaxBackgroundImage:"",parallaxBackgroundSize:"",parallaxBackgroundRepeat:"",parallaxBackgroundPosition:"",parallaxBackgroundHorizontal:null,parallaxBackgroundVertical:null,pdfMaxPagesPerSlide:Number.POSITIVE_INFINITY,pdfSeparateFragments:!0,pdfPageHeightOffset:-1,viewDistance:3,display:"block",dependencies:[]},L=!1,S=!1,t=!1,n=null,i=null,r=!1,o=!1,E=[],c=1,d={layout:"",overview:""},x={},q={},N=0,M=0,I=!1,T=0,C=0,P=-1,B=!1,H={startX:0,startY:0,startSpan:0,startCount:0,captured:!1,threshold:40},R={"N , SPACE":"Next slide",P:"Previous slide","← , H":"Navigate left","→ , L":"Navigate right","↑ , K":"Navigate up","↓ , J":"Navigate down",Home:"First slide",End:"Last slide","B , .":"Pause",F:"Fullscreen","ESC, O":"Slide overview"},D={};function z(e){var t="";if(3===e.nodeType)t+=e.textContent;else if(1===e.nodeType){var r=e.getAttribute("aria-hidden"),n="none"===window.getComputedStyle(e).display;"true"===r||n||K(e.childNodes).forEach(function(e){t+=z(e)})}return t}function O(){var e=ue(window.innerWidth,window.innerHeight),v=Math.floor(e.width*(1+A.margin)),g=Math.floor(e.height*(1+A.margin)),m=e.width,b=e.height;Z("@page{size:"+v+"px "+g+"px; margin: 0px;}"),Z(".reveal section>img, .reveal section>video, .reveal section>iframe{max-width: "+m+"px; max-height:"+b+"px}"),document.body.classList.add("print-pdf"),document.body.style.width=v+"px",document.body.style.height=g+"px",de(m,b),K(x.wrapper.querySelectorAll(y)).forEach(function(e,r){e.setAttribute("data-index-h",r),e.classList.contains("stack")&&K(e.querySelectorAll("section")).forEach(function(e,t){e.setAttribute("data-index-h",r),e.setAttribute("data-index-v",t)})}),K(x.wrapper.querySelectorAll(s)).forEach(function(e){if(!1===e.classList.contains("stack")){var t=(v-m)/2,r=(g-b)/2,n=e.scrollHeight,a=Math.max(Math.ceil(n/g),1);(1===(a=Math.min(a,A.pdfMaxPagesPerSlide))&&A.center||e.classList.contains("center"))&&(r=Math.max((g-n)/2,0));var i=document.createElement("div");if(i.className="pdf-page",i.style.height=(g+A.pdfPageHeightOffset)*a+"px",e.parentNode.insertBefore(i,e),i.appendChild(e),e.style.left=t+"px",e.style.top=r+"px",e.style.width=m+"px",e.slideBackgroundElement&&i.insertBefore(e.slideBackgroundElement,e),A.showNotes){var o=nt(e);if(o){var s="string"==typeof A.showNotes?A.showNotes:"inline",l=document.createElement("div");l.classList.add("speaker-notes"),l.classList.add("speaker-notes-pdf"),l.setAttribute("data-layout",s),l.innerHTML=o,"separate-page"===s?i.parentNode.insertBefore(l,i.nextSibling):(l.style.left="8px",l.style.bottom="8px",l.style.width=v-16+"px",i.appendChild(l))}}if(A.slideNumber&&/all|print/i.test(A.showSlideNumber)){var c=parseInt(e.getAttribute("data-index-h"),10)+1,d=parseInt(e.getAttribute("data-index-v"),10)+1,u=document.createElement("div");u.classList.add("slide-number"),u.classList.add("slide-number-pdf"),u.innerHTML=Re(c,".",d),i.appendChild(u)}if(A.pdfSeparateFragments){var p,f,h=it(i.querySelectorAll(".fragment"),!0);h.forEach(function(e){p&&p.forEach(function(e){e.classList.remove("current-fragment")}),e.forEach(function(e){e.classList.add("visible","current-fragment")});var t=i.cloneNode(!0);i.parentNode.insertBefore(t,(f||i).nextSibling),p=e,f=t}),h.forEach(function(e){e.forEach(function(e){e.classList.remove("visible","current-fragment")})})}else K(i.querySelectorAll(".fragment:not(.fade-out)")).forEach(function(e){e.classList.add("visible")})}}),ne("pdf-ready")}function W(e,t,r,n){for(var a=e.querySelectorAll("."+r),i=0;i"+R[t]+"";for(var r in D)D[r].key&&D[r].description&&(e+=""+D[r].key+""+D[r].description+"");e+="",x.overlay.innerHTML=["
",'',"
",'
','
'+e+"
","
"].join(""),x.overlay.querySelector(".close").addEventListener("click",function(e){le(),e.preventDefault()},!1),setTimeout(function(){x.overlay.classList.add("visible")},1)}}function le(){x.overlay&&(x.overlay.parentNode.removeChild(x.overlay),x.overlay=null)}function ce(){if(x.wrapper&&!te()){if(!A.disableLayout){var e=ue();de(A.width,A.height),x.slides.style.width=e.width+"px",x.slides.style.height=e.height+"px",c=Math.min(e.presentationWidth/e.width,e.presentationHeight/e.height),c=Math.max(c,A.minScale),1===(c=Math.min(c,A.maxScale))?(x.slides.style.zoom="",x.slides.style.left="",x.slides.style.top="",x.slides.style.bottom="",Q({layout:x.slides.style.right=""})):1 .stretch")).forEach(function(e){var t=function i(e,t){if(t=t||0,e){var r,n=e.style.height;return e.style.height="0px",r=t-e.parentNode.offsetHeight,e.style.height=n+"px",r}return t}(e,s);if(/(img|video)/gi.test(e.nodeName)){var r=e.naturalWidth||e.videoWidth,n=e.naturalHeight||e.videoHeight,a=Math.min(o/r,t/n);e.style.width=r*a+"px",e.style.height=n*a+"px"}else e.style.width=o+"px",e.style.height=t+"px"})}function ue(e,t){var r={width:A.width,height:A.height,presentationWidth:e||x.wrapper.offsetWidth,presentationHeight:t||x.wrapper.offsetHeight};return r.presentationWidth-=r.presentationWidth*A.margin,r.presentationHeight-=r.presentationHeight*A.margin,"string"==typeof r.width&&/%$/.test(r.width)&&(r.width=parseInt(r.width,10)/100*r.presentationWidth),"string"==typeof r.height&&/%$/.test(r.height)&&(r.height=parseInt(r.height,10)/100*r.presentationHeight),r}function pe(e,t){"object"==typeof e&&"function"==typeof e.setAttribute&&e.setAttribute("data-previous-indexv",t||0)}function fe(e){if("object"==typeof e&&"function"==typeof e.setAttribute&&e.classList.contains("stack")){var t=e.hasAttribute("data-start-indexv")?"data-start-indexv":"data-previous-indexv";return parseInt(e.getAttribute(t)||0,10)}return 0}function he(){if(A.overview&&!ye()){t=!0,x.wrapper.classList.add("overview"),x.wrapper.classList.remove("overview-deactivating"),q.overviewTransitions&&setTimeout(function(){x.wrapper.classList.add("overview-animated")},1),dt(),x.slides.appendChild(x.background),K(x.wrapper.querySelectorAll(s)).forEach(function(e){e.classList.contains("stack")||e.addEventListener("click",Wt,!0)});var e=ue();n=e.width+70,i=e.height+70,A.rtl&&(n=-n),Ce(),ve(),ge(),ce(),ne("overviewshown",{indexh:h,indexv:v,currentSlide:m})}}function ve(){K(x.wrapper.querySelectorAll(y)).forEach(function(e,r){e.setAttribute("data-index-h",r),J(e,"translate3d("+r*n+"px, 0, 0)"),e.classList.contains("stack")&&K(e.querySelectorAll("section")).forEach(function(e,t){e.setAttribute("data-index-h",r),e.setAttribute("data-index-v",t),J(e,"translate3d(0, "+t*i+"px, 0)")})}),K(x.background.childNodes).forEach(function(e,t){J(e,"translate3d("+t*n+"px, 0, 0)"),K(e.querySelectorAll(".slide-background")).forEach(function(e,t){J(e,"translate3d(0, "+t*i+"px, 0)")})})}function ge(){var e=Math.min(window.innerWidth,window.innerHeight);Q({overview:["scale("+Math.max(e/5,150)/e+")","translateX("+-h*n+"px)","translateY("+-v*i+"px)"].join(" ")})}function me(){A.overview&&(t=!1,x.wrapper.classList.remove("overview"),x.wrapper.classList.remove("overview-animated"),x.wrapper.classList.add("overview-deactivating"),setTimeout(function(){x.wrapper.classList.remove("overview-deactivating")},1),x.wrapper.appendChild(x.background),K(x.wrapper.querySelectorAll(s)).forEach(function(e){J(e,""),e.removeEventListener("click",Wt,!0)}),K(x.background.querySelectorAll(".slide-background")).forEach(function(e){J(e,"")}),Q({overview:""}),qe(h,v),ce(),ct(),ne("overviewhidden",{indexh:h,indexv:v,currentSlide:m}))}function be(e){"boolean"==typeof e?e?he():me():ye()?me():he()}function ye(){return t}function we(){var e,t="/",r=m?m.getAttribute("id"):null;if(r&&(r=encodeURIComponent(r)),A.fragmentInURL&&(e=Ze().f),"string"==typeof r&&r.length&&e===undefined)t="/"+r;else{var n=A.hashOneBasedIndex?1:0;(0section>section").length?x.wrapper.classList.add("has-vertical-slides"):x.wrapper.classList.remove("has-vertical-slides"),1section").length?x.wrapper.classList.add("has-horizontal-slides"):x.wrapper.classList.remove("has-horizontal-slides")}}function Pe(){A.showNotes&&x.speakerNotes&&m&&!te()&&(x.speakerNotes.innerHTML=nt()||'No notes on this slide.')}function Be(){A.progress&&x.progressbar&&(x.progressbar.style.width=e()*x.wrapper.offsetWidth+"px")}function He(){if(A.slideNumber&&x.slideNumber){var e=[],t="h.v";switch("string"==typeof A.slideNumber&&(t=A.slideNumber),/c/.test(t)||1!==x.wrapper.querySelectorAll(y).length||(t="c"),t){case"c":e.push($e()+1);break;case"c/t":e.push($e()+1,"/",et());break;case"h/v":e.push(h+1),ke()&&e.push("/",v+1);break;default:e.push(h+1),ke()&&e.push(".",v+1)}x.slideNumber.innerHTML=Re(e[0],e[1],e[2])}}function Re(e,t,r){var n="#"+we();return"number"!=typeof r||isNaN(r)?''+e+"":''+e+''+t+''+r+""}function De(){var e=Ue(),t=Ye();x.controlsLeft.concat(x.controlsRight).concat(x.controlsUp).concat(x.controlsDown).concat(x.controlsPrev).concat(x.controlsNext).forEach(function(e){e.classList.remove("enabled"),e.classList.remove("fragmented"),e.setAttribute("disabled","disabled")}),e.left&&x.controlsLeft.forEach(function(e){e.classList.add("enabled"),e.removeAttribute("disabled")}),e.right&&x.controlsRight.forEach(function(e){e.classList.add("enabled"),e.removeAttribute("disabled")}),e.up&&x.controlsUp.forEach(function(e){e.classList.add("enabled"),e.removeAttribute("disabled")}),e.down&&x.controlsDown.forEach(function(e){e.classList.add("enabled"),e.removeAttribute("disabled")}),(e.left||e.up)&&x.controlsPrev.forEach(function(e){e.classList.add("enabled"),e.removeAttribute("disabled")}),(e.right||e.down)&&x.controlsNext.forEach(function(e){e.classList.add("enabled"),e.removeAttribute("disabled")}),m&&(t.prev&&x.controlsPrev.forEach(function(e){e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")}),t.next&&x.controlsNext.forEach(function(e){e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")}),ke(m)?(t.prev&&x.controlsUp.forEach(function(e){e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")}),t.next&&x.controlsDown.forEach(function(e){e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")})):(t.prev&&x.controlsLeft.forEach(function(e){e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")}),t.next&&x.controlsRight.forEach(function(e){e.classList.add("fragmented","enabled"),e.removeAttribute("disabled")}))),A.controlsTutorial&&(!o&&e.down?x.controlsDownArrow.classList.add("highlight"):(x.controlsDownArrow.classList.remove("highlight"),!r&&e.right&&0===v?x.controlsRightArrow.classList.add("highlight"):x.controlsRightArrow.classList.remove("highlight")))}function ze(t){var n=null,a=A.rtl?"future":"past",i=A.rtl?"past":"future";if(K(x.background.childNodes).forEach(function(e,r){e.classList.remove("past"),e.classList.remove("present"),e.classList.remove("future"),r'}),n.appendChild(c)}else if(l&&!0!==t.excludeIframes){var d=document.createElement("iframe");d.setAttribute("allowfullscreen",""),d.setAttribute("mozallowfullscreen",""),d.setAttribute("webkitallowfullscreen",""),/autoplay=(1|true|yes)/gi.test(l)?d.setAttribute("data-src",l):d.setAttribute("src",l),d.style.width="100%",d.style.height="100%",d.style.maxHeight="100%",d.style.maxWidth="100%",n.appendChild(d)}}}}function Fe(e){e.style.display="none";var t=rt(e);t&&(t.style.display="none"),K(e.querySelectorAll("video[data-lazy-loaded][src], audio[data-lazy-loaded][src]")).forEach(function(e){e.setAttribute("data-src",e.getAttribute("src")),e.removeAttribute("src")}),K(e.querySelectorAll("video[data-lazy-loaded] source[src], audio source[src]")).forEach(function(e){e.setAttribute("data-src",e.getAttribute("src")),e.removeAttribute("src")})}function Ue(){var e=x.wrapper.querySelectorAll(y),t=x.wrapper.querySelectorAll(b),r={left:0T&&(T=1e3*e.duration/e.playbackRate+1e3)}),!T||B||Ee()||ye()||u.isLastSlide()&&!Ye().next&&!0!==A.loop||(C=setTimeout(function(){"function"==typeof A.autoSlideMethod?A.autoSlideMethod():bt(),ct()},T),P=Date.now()),a&&a.setPlaying(-1!==C)}}function dt(){clearTimeout(C),C=-1}function ut(){T&&!B&&(B=!0,ne("autoslidepaused"),clearTimeout(C),a&&a.setPlaying(!1))}function pt(){T&&B&&(B=!1,ne("autoslideresumed"),ct())}function ft(){A.rtl?(ye()||!1===st())&&Ue().left&&qe(h+1):(ye()||!1===lt())&&Ue().left&&qe(h-1)}function ht(){r=!0,A.rtl?(ye()||!1===lt())&&Ue().right&&qe(h-1):(ye()||!1===st())&&Ue().right&&qe(h+1)}function vt(){(ye()||!1===lt())&&Ue().up&&qe(h,v-1)}function gt(){o=!0,(ye()||!1===st())&&Ue().down&&qe(h,v+1)}function mt(){var e;if(!1===lt())if(Ue().up)vt();else if(e=A.rtl?K(x.wrapper.querySelectorAll(y+".future")).pop():K(x.wrapper.querySelectorAll(y+".past")).pop()){var t=e.querySelectorAll("section").length-1||undefined;qe(h-1,t)}}function bt(){if(!(o=r=!0)===st()){var e=Ue();e.down&&e.right&&A.loop&&u.isLastVerticalSlide(m)&&(e.down=!1),e.down?gt():A.rtl?ft():ht()}}function yt(e){for(;e&&"function"==typeof e.hasAttribute;){if(e.hasAttribute("data-prevent-swipe"))return!0;e=e.parentNode}return!1}function wt(e){A.autoSlideStoppable&&ut()}function kt(e){e.shiftKey&&63===e.charCode&&oe()}function At(e){if("function"==typeof A.keyboardCondition&&!1===A.keyboardCondition(e))return!0;var t=B;wt();var r=document.activeElement&&"inherit"!==document.activeElement.contentEditable,n=document.activeElement&&document.activeElement.tagName&&/input|textarea/i.test(document.activeElement.tagName),a=document.activeElement&&document.activeElement.className&&/speaker-notes/i.test(document.activeElement.className);if(!(r||n||a||e.shiftKey&&32!==e.keyCode||e.altKey||e.ctrlKey||e.metaKey)){var i,o=[66,86,190,191];if("object"==typeof A.keyboard)for(i in A.keyboard)"togglePause"===A.keyboard[i]&&o.push(parseInt(i,10));if(Ee()&&-1===o.indexOf(e.keyCode))return!1;var s=!1;if("object"==typeof A.keyboard)for(i in A.keyboard)if(parseInt(i,10)===e.keyCode){var l=A.keyboard[i];"function"==typeof l?l.apply(null,[e]):"string"==typeof l&&"function"==typeof u[l]&&u[l].call(),s=!0}if(!1===s)for(i in D)if(parseInt(i,10)===e.keyCode){var c=D[i].callback;"function"==typeof c?c.apply(null,[e]):"string"==typeof c&&"function"==typeof u[c]&&u[c].call(),s=!0}if(!1===s)switch(s=!0,e.keyCode){case 80:case 33:mt();break;case 78:case 34:bt();break;case 72:case 37:ft();break;case 76:case 39:ht();break;case 75:case 38:vt();break;case 74:case 40:gt();break;case 36:qe(0);break;case 35:qe(Number.MAX_VALUE);break;case 32:ye()?me():e.shiftKey?mt():bt();break;case 13:ye()?me():s=!1;break;case 58:case 59:case 66:case 86:case 190:case 191:Se();break;case 70:!function d(){var e=document.documentElement,t=e.requestFullscreen||e.webkitRequestFullscreen||e.webkitRequestFullScreen||e.mozRequestFullScreen||e.msRequestFullscreen;t&&t.apply(e)}();break;case 65:A.autoSlideStoppable&&xe(t);break;default:s=!1}s?e.preventDefault&&e.preventDefault():27!==e.keyCode&&79!==e.keyCode||!q.transforms3d||(x.overlay?le():be(),e.preventDefault&&e.preventDefault()),ct()}}function Lt(e){if(yt(e.target))return!0;H.startX=e.touches[0].clientX,H.startY=e.touches[0].clientY,H.startCount=e.touches.length,2===e.touches.length&&A.overview&&(H.startSpan=_({x:e.touches[1].clientX,y:e.touches[1].clientY},{x:H.startX,y:H.startY}))}function St(e){if(yt(e.target))return!0;if(H.captured)k.match(/android/gi)&&e.preventDefault();else{wt();var t=e.touches[0].clientX,r=e.touches[0].clientY;if(2===e.touches.length&&2===H.startCount&&A.overview){var n=_({x:e.touches[1].clientX,y:e.touches[1].clientY},{x:H.startX,y:H.startY});Math.abs(H.startSpan-n)>H.threshold&&(H.captured=!0,nH.threshold&&Math.abs(a)>Math.abs(i)?(H.captured=!0,ft()):a<-H.threshold&&Math.abs(a)>Math.abs(i)?(H.captured=!0,ht()):i>H.threshold?(H.captured=!0,vt()):i<-H.threshold&&(H.captured=!0,gt()),A.embedded?(H.captured||ke(m))&&e.preventDefault():e.preventDefault()}}}function Et(e){H.captured=!1}function xt(e){e.pointerType!==e.MSPOINTER_TYPE_TOUCH&&"touch"!==e.pointerType||(e.touches=[{clientX:e.clientX,clientY:e.clientY}],Lt(e))}function qt(e){e.pointerType!==e.MSPOINTER_TYPE_TOUCH&&"touch"!==e.pointerType||(e.touches=[{clientX:e.clientX,clientY:e.clientY}],St(e))}function Nt(e){e.pointerType!==e.MSPOINTER_TYPE_TOUCH&&"touch"!==e.pointerType||(e.touches=[{clientX:e.clientX,clientY:e.clientY}],Et())}function Mt(e){if(600",'','',"",'
','
','','','Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).',"","
"].join(""),x.overlay.querySelector("iframe").addEventListener("load",function(e){x.overlay.classList.add("loaded")},!1),x.overlay.querySelector(".close").addEventListener("click",function(e){le(),e.preventDefault()},!1),x.overlay.querySelector(".external").addEventListener("click",function(e){le()},!1),setTimeout(function(){x.overlay.classList.add("visible")},1)}(t),e.preventDefault())}}function Ut(e){u.isLastSlide()&&!1===A.loop?(qe(0,0),pt()):B?pt():ut()}function Yt(e,t){this.diameter=100,this.diameter2=this.diameter/2,this.thickness=6,this.playing=!1,this.progress=0,this.progressOffset=1,this.container=e,this.progressCheck=t,this.canvas=document.createElement("canvas"),this.canvas.className="playback",this.canvas.width=this.diameter,this.canvas.height=this.diameter,this.canvas.style.width=this.diameter2+"px",this.canvas.style.height=this.diameter2+"px",this.context=this.canvas.getContext("2d"),this.container.appendChild(this.canvas),this.render()}return Yt.prototype.setPlaying=function(e){var t=this.playing;this.playing=e,!t&&this.playing?this.animate():this.render()},Yt.prototype.animate=function(){var e=this.progress;this.progress=this.progressCheck(),.8"),x.progressbar=x.progress.querySelector("span"),x.controls=W(x.wrapper,"aside","controls",''),x.slideNumber=W(x.wrapper,"div","slide-number",""),x.speakerNotes=W(x.wrapper,"div","speaker-notes",null),x.speakerNotes.setAttribute("data-prevent-swipe",""),x.speakerNotes.setAttribute("tabindex","0"),x.pauseOverlay=W(x.wrapper,"div","pause-overlay",''),x.resumeButton=x.pauseOverlay.querySelector(".resume-button"),x.wrapper.setAttribute("role","application"),x.controlsLeft=K(document.querySelectorAll(".navigate-left")),x.controlsRight=K(document.querySelectorAll(".navigate-right")),x.controlsUp=K(document.querySelectorAll(".navigate-up")),x.controlsDown=K(document.querySelectorAll(".navigate-down")),x.controlsPrev=K(document.querySelectorAll(".navigate-prev")),x.controlsNext=K(document.querySelectorAll(".navigate-next")),x.controlsRightArrow=x.controls.querySelector(".navigate-right"),x.controlsDownArrow=x.controls.querySelector(".navigate-down"),x.statusDiv=function t(){var e=document.getElementById("aria-status-div");return e||((e=document.createElement("div")).style.position="absolute",e.style.height="1px",e.style.width="1px",e.style.overflow="hidden",e.style.clip="rect( 1px, 1px, 1px, 1px )",e.setAttribute("id","aria-status-div"),e.setAttribute("aria-live","polite"),e.setAttribute("aria-atomic","true"),x.wrapper.appendChild(e)),e}()}(),function t(){A.postMessage&&window.addEventListener("message",function(e){var t=e.data;"string"==typeof t&&"{"===t.charAt(0)&&"}"===t.charAt(t.length-1)&&(t=JSON.parse(t)).method&&"function"==typeof u[t.method]&&u[t.method].apply(u,t.args)},!1)}(),function r(){setInterval(function(){0===x.wrapper.scrollTop&&0===x.wrapper.scrollLeft||(x.wrapper.scrollTop=0,x.wrapper.scrollLeft=0)},1e3)}(),function n(){K(x.wrapper.querySelectorAll(y)).forEach(function(e){var t=K(e.querySelectorAll("section"));t.forEach(function(e,t){0 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; 10 | overflow-x: auto; 11 | padding: 0.5em; 12 | background: #3f3f3f; 13 | color: #dcdcdc; 14 | } 15 | 16 | .hljs-keyword, 17 | .hljs-selector-tag, 18 | .hljs-tag { 19 | color: #e3ceab; 20 | } 21 | 22 | .hljs-template-tag { 23 | color: #dcdcdc; 24 | } 25 | 26 | .hljs-number { 27 | color: #8cd0d3; 28 | } 29 | 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-attribute { 33 | color: #efdcbc; 34 | } 35 | 36 | .hljs-literal { 37 | color: #efefaf; 38 | } 39 | 40 | .hljs-subst { 41 | color: #8f8f8f; 42 | } 43 | 44 | .hljs-title, 45 | .hljs-name, 46 | .hljs-selector-id, 47 | .hljs-selector-class, 48 | .hljs-section, 49 | .hljs-type { 50 | color: #efef8f; 51 | } 52 | 53 | .hljs-symbol, 54 | .hljs-bullet, 55 | .hljs-link { 56 | color: #dca3a3; 57 | } 58 | 59 | .hljs-deletion, 60 | .hljs-string, 61 | .hljs-built_in, 62 | .hljs-builtin-name { 63 | color: #cc9393; 64 | } 65 | 66 | .hljs-addition, 67 | .hljs-comment, 68 | .hljs-quote, 69 | .hljs-meta { 70 | color: #7f9f7f; 71 | } 72 | 73 | 74 | .hljs-emphasis { 75 | font-style: italic; 76 | } 77 | 78 | .hljs-strong { 79 | font-weight: bold; 80 | } 81 | -------------------------------------------------------------------------------- /slides/lib/js/classList.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ 2 | if(typeof document!=="undefined"&&!("classList" in document.createElement("a"))){(function(j){var a="classList",f="prototype",m=(j.HTMLElement||j.Element)[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;pn?(i.screensCss.gt&&r("gt-"+n),i.screensCss.gte&&r("gte-"+n)):tt);u.feature("landscape",fe?(i.browserCss.gt&&r("gt-"+f+e),i.browserCss.gte&&r("gte-"+f+e)):h2&&this[u+1]!==t)u&&r(this.slice(u,u+1).join("-").toLowerCase()+i.section);else{var f=n||"index",e=f.indexOf(".");e>0&&(f=f.substring(0,e));c.id=f.toLowerCase()+i.page;u||r("root"+i.section)}});u.screen={height:n.screen.height,width:n.screen.width};tt();b=0;n.addEventListener?n.addEventListener("resize",it,!1):n.attachEvent("onresize",it)})(window); 3 | /*! head.css3 - v1.0.0 */ 4 | (function(n,t){"use strict";function a(n){for(var r in n)if(i[n[r]]!==t)return!0;return!1}function r(n){var t=n.charAt(0).toUpperCase()+n.substr(1),i=(n+" "+c.join(t+" ")+t).split(" ");return!!a(i)}var h=n.document,o=h.createElement("i"),i=o.style,s=" -o- -moz- -ms- -webkit- -khtml- ".split(" "),c="Webkit Moz O ms Khtml".split(" "),l=n.head_conf&&n.head_conf.head||"head",u=n[l],f={gradient:function(){var n="background-image:";return i.cssText=(n+s.join("gradient(linear,left top,right bottom,from(#9f9),to(#fff));"+n)+s.join("linear-gradient(left top,#eee,#fff);"+n)).slice(0,-n.length),!!i.backgroundImage},rgba:function(){return i.cssText="background-color:rgba(0,0,0,0.5)",!!i.backgroundColor},opacity:function(){return o.style.opacity===""},textshadow:function(){return i.textShadow===""},multiplebgs:function(){i.cssText="background:url(https://),url(https://),red url(https://)";var n=(i.background||"").match(/url/g);return Object.prototype.toString.call(n)==="[object Array]"&&n.length===3},boxshadow:function(){return r("boxShadow")},borderimage:function(){return r("borderImage")},borderradius:function(){return r("borderRadius")},cssreflections:function(){return r("boxReflect")},csstransforms:function(){return r("transform")},csstransitions:function(){return r("transition")},touch:function(){return"ontouchstart"in n},retina:function(){return n.devicePixelRatio>1},fontface:function(){var t=u.browser.name,n=u.browser.version;switch(t){case"ie":return n>=9;case"chrome":return n>=13;case"ff":return n>=6;case"ios":return n>=5;case"android":return!1;case"webkit":return n>=5.1;case"opera":return n>=10;default:return!1}}};for(var e in f)f[e]&&u.feature(e,f[e].call(),!0);u.feature()})(window); 5 | /*! head.load - v1.0.3 */ 6 | (function(n,t){"use strict";function w(){}function u(n,t){if(n){typeof n=="object"&&(n=[].slice.call(n));for(var i=0,r=n.length;i/g, ''); 12 | result = result.replace(/&/g, '
  • '); 14 | return result; 15 | } 16 | 17 | const revealSlides = slides.map(slide => { 18 | return ` 25 | 32 | ${mdRender(slide)} 33 | 34 | `; 35 | }); 36 | 37 | const template = fs.readFileSync('index.tpl.html', { encoding: 'utf8' }); 38 | 39 | fs.writeFileSync( 40 | 'index.html', 41 | template.replace('${CONTENT}', revealSlides.join('\n')) 42 | ); 43 | -------------------------------------------------------------------------------- /slides/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slides", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "node md2reveal", 8 | "serve": "yarn build && serve" 9 | }, 10 | "devDependencies": { 11 | "markdown-it": "^10.0.0", 12 | "serve": "^11.2.0" 13 | } 14 | } -------------------------------------------------------------------------------- /tasks/1/README.md: -------------------------------------------------------------------------------- 1 | # Task 1 2 | 3 | ## Goal 4 | 5 | Create a `Hero` component that shows the first name, last name and the framework, and also a checkbox synced to the `selected` property, and sends a `select` event when clicked, having the payload `{ id, value }`, where `id` is the hero's ID, and `value` is the new `selected` value. 6 | 7 | ## Hints 8 | 9 | - use the following markup: 10 | 11 | ```html 12 |
    First Name
    13 |
    Last Name
    14 |
    Framework
    15 |
    16 | ``` 17 | -------------------------------------------------------------------------------- /tasks/1/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /tasks/1/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest', 8 | }, 9 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\]'], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1', 12 | }, 13 | snapshotSerializers: ['jest-serializer-vue'], 14 | testMatch: [ 15 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)', 16 | ], 17 | testURL: 'http://localhost/', 18 | }; 19 | -------------------------------------------------------------------------------- /tasks/1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task1", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "test:unit": "vue-cli-service test:unit" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.4.0", 13 | "vue": "^2.6.10" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^4.0.5", 17 | "@vue/cli-plugin-eslint": "^4.0.5", 18 | "@vue/cli-plugin-unit-jest": "^4.0.5", 19 | "@vue/cli-service": "^4.0.5", 20 | "@vue/eslint-config-prettier": "^5.0.0", 21 | "@vue/test-utils": "1.0.0-beta.29", 22 | "babel-core": "7.0.0-bridge.0", 23 | "babel-eslint": "^10.0.3", 24 | "babel-jest": "^24.9.0", 25 | "eslint": "^6.6.0", 26 | "eslint-plugin-prettier": "^3.1.1", 27 | "eslint-plugin-vue": "^6.0.0", 28 | "vue-template-compiler": "^2.6.10" 29 | } 30 | } -------------------------------------------------------------------------------- /tasks/1/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbogdan/vue-advanced-components-workshop/8b3e708df295e5fb920915a0f5d34858dfca7f3d/tasks/1/public/favicon.ico -------------------------------------------------------------------------------- /tasks/1/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | repo 9 | 10 | 11 | 14 |
    15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tasks/1/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /tasks/1/src/components/Hero.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 25 | -------------------------------------------------------------------------------- /tasks/1/src/components/HeroList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 71 | 72 | 77 | -------------------------------------------------------------------------------- /tasks/1/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app'); 9 | -------------------------------------------------------------------------------- /tasks/1/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /tasks/1/tests/unit/Hero.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Hero from '@/components/Hero.vue'; 3 | 4 | let wrapper; 5 | 6 | afterEach(() => { 7 | wrapper.destroy(); 8 | }); 9 | 10 | function createWrapper(onSelect) { 11 | return mount(Hero, { 12 | propsData: { 13 | data: { 14 | id: 1, 15 | firstName: 'Bogdan', 16 | lastName: 'Luca', 17 | framework: 'Vue.js', 18 | selected: false, 19 | }, 20 | }, 21 | listeners: { 22 | select: onSelect || function() {}, 23 | }, 24 | }); 25 | } 26 | 27 | describe('Hero.vue', () => { 28 | it('shows the hero data', () => { 29 | wrapper = createWrapper(); 30 | expect(wrapper.text()).toContain('Bogdan'); 31 | expect(wrapper.text()).toContain('Luca'); 32 | expect(wrapper.text()).toContain('Vue.js'); 33 | }); 34 | 35 | it('shows a checkbox', () => { 36 | wrapper = createWrapper(); 37 | const $checkbox = wrapper.find('input'); 38 | expect($checkbox.exists()).toBe(true); 39 | expect($checkbox.element.type).toBe('checkbox'); 40 | }); 41 | 42 | it('updates the checkbox when "selected" property changes', () => { 43 | wrapper = createWrapper(); 44 | wrapper.vm.data.selected = true; 45 | const $checkbox = wrapper.find('input'); 46 | expect($checkbox.element.checked).toBe(true); 47 | }); 48 | 49 | it('emits a "select" event with the specified payload when clicking the checkbox', () => { 50 | const onSelect = jest.fn(); 51 | wrapper = createWrapper(onSelect); 52 | const $checkbox = wrapper.find('input'); 53 | $checkbox.trigger('click'); 54 | expect(onSelect).toHaveBeenCalledWith({ id: 1, value: true }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tasks/2/README.md: -------------------------------------------------------------------------------- 1 | # Task 2 2 | 3 | ## Goal 4 | 5 | Extract the API fetch logic from `PeopleList` and `PlanetList` to a mixin. 6 | 7 | ## Hints 8 | 9 | - the mixin should manage its own `data()` 10 | - the mixin should define a method that will be called from the components using it 11 | -------------------------------------------------------------------------------- /tasks/2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.0", 12 | "vue": "^2.6.10" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "^4.0.5", 16 | "@vue/cli-plugin-eslint": "^4.0.5", 17 | "@vue/cli-service": "^4.0.5", 18 | "@vue/eslint-config-prettier": "^5.0.0", 19 | "babel-eslint": "^10.0.3", 20 | "eslint": "^6.6.0", 21 | "eslint-plugin-prettier": "^3.1.1", 22 | "eslint-plugin-vue": "^6.0.0", 23 | "vue-template-compiler": "^2.6.10" 24 | } 25 | } -------------------------------------------------------------------------------- /tasks/2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbogdan/vue-advanced-components-workshop/8b3e708df295e5fb920915a0f5d34858dfca7f3d/tasks/2/public/favicon.ico -------------------------------------------------------------------------------- /tasks/2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | repo 9 | 10 | 11 | 14 |
    15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tasks/2/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /tasks/2/src/components/APIFetchMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // TODO 3 | }; 4 | -------------------------------------------------------------------------------- /tasks/2/src/components/PeopleList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | -------------------------------------------------------------------------------- /tasks/2/src/components/PlanetList.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | -------------------------------------------------------------------------------- /tasks/2/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app'); 9 | -------------------------------------------------------------------------------- /tasks/3/README.md: -------------------------------------------------------------------------------- 1 | # Task 3 2 | 3 | ## Goal 4 | 5 | Create a `SelectOther` component that receives an array of strings called `options`. There should be a `select` element, having all the provided options plus an additional `Other` option, that when selected, shows an input where you can type a custom value. Make the component work with `v-model`, i.e. 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | ## Bonus 12 | 13 | - when the `v-model`'s value is `null`, clear the select 14 | 15 | ## Hints 16 | 17 | - use a computed property with a getter and a setter for `