├── .gitignore ├── README.md ├── package.json ├── pt.1-jQuery-to-React ├── .eslintrc ├── .flowconfig ├── .vscode │ └── settings.json ├── NOTES.md ├── README.md ├── jQuery │ ├── imperative │ │ ├── index.html │ │ └── index.js │ ├── index.html │ ├── style.css │ └── template │ │ ├── index.html │ │ ├── index.js │ │ └── jquery.index.js ├── javascript │ ├── index.html │ ├── server.html │ └── server.js ├── package.json ├── php │ ├── err.log │ ├── fn.php │ ├── index.php │ └── template.php └── yarn.lock ├── pt.2-React ├── README.md ├── basic.html ├── counter.html ├── index.html └── text.html ├── pt.3-React-prakticky └── README.md ├── pt.4-React-sprava-stavu ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.css │ ├── App.test.js │ ├── clock.js │ ├── index.css │ ├── index.js │ ├── initial-state.js │ ├── just-component │ │ └── just-component.js │ ├── logo.svg │ ├── stateful-component │ │ └── stateful-component.js │ ├── stateless-component │ │ ├── business-logic.js │ │ └── stateless-component.js │ └── todo-form │ │ ├── form.js │ │ └── todo.js └── yarn.lock ├── pt.5-React-redux ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── actions │ │ └── index.js │ ├── components │ │ ├── invoice-form.js │ │ ├── invoice-table.js │ │ ├── modal.js │ │ └── taxes-calculator.js │ ├── index.css │ ├── index.js │ ├── registerServiceWorker.js │ └── store │ │ ├── index.js │ │ └── reducer.js └── yarn.lock ├── pt.6-React-advanced-redux ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── actions │ │ └── index.js │ ├── components │ │ ├── invoice-form.js │ │ ├── invoice-table.js │ │ ├── lazy-load.js │ │ ├── modal.js │ │ └── taxes-calculator.js │ ├── index.css │ ├── index.js │ ├── registerServiceWorker.js │ ├── store │ │ ├── connect.js │ │ ├── index.js │ │ ├── logger-middleware.js │ │ ├── mini-redux.js │ │ ├── reducer.js │ │ └── store-provider.js │ └── utils.js └── yarn.lock ├── styleguide ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ │ ├── button.js │ │ └── form.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── utils.js └── yarn.lock └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | **/.DS_Store 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nautč se React.js](https://image.ibb.co/dnb7tv/react_event_fb_title_v03.png) 2 | 3 | # Materiálů ke kurzu Naučte se React.js! 4 | 5 | Kurz je rozdělen na několik částí: 6 | 7 | 1. [Od jQuery po React](./pt.1-jQuery-to-React) (Vojta Tranta [@iVojta](https://twitter.com/ivojta)) 8 | 2. [Konečně React](./pt.2-React) (Vojta Tranta [@iVojta](https://twitter.com/ivojta)) 9 | 3. [React prakticky](./pt.3-React-prakticky) (Petr Brzek [@petrbrzek](https://twitter.com/petrbrzek)) 10 | 4. [React - správa stavu aplikace](./pt.4-React-sprava-stavu) (Vojta Tranta [@iVojta](https://twitter.com/ivojta)) 11 | 5. [React - Redux](./pt.5-React-redux) (Vojta Tranta [@iVojta](https://twitter.com/ivojta)) 12 | 6. [React - advanced Redux](./pt.6-React-advanced-redux) (Vojta Tranta [@iVojta](https://twitter.com/ivojta)) 13 | 14 | 15 | ## Autoři 16 | Kurz byl vytvořen pro [WebDev](https://www.facebook.com/groups/webdevjs) srazy nadšenců do webového vývoje. Pokud chceme materiály použít, nezapomeňte zmínit jména původních autorů. 17 | 18 | Pull requesty jsou vítány! 19 | 20 | Autoři: 21 | - (Vojta Tranta [@iVojta](https://twitter.com/ivojta)) 22 | - Petr Brzek 23 | 24 | 25 | Podívejte se i na naše [ostatní kurzy a materiály](https://github.com/webdev-js-evenings). 26 | 27 | WebDev kurzy založil [Nikita Mironov](https://www.facebook.com/why7e) 28 | 29 | Za poskytnutí prostor děkujeme [@Avocode](https://avocode.com/) 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "create-react-app": "^1.2.0", 4 | "express": "^4.14.1", 5 | "firebase": "^4.1.2", 6 | "react": "^15.5.4", 7 | "react-dom": "^15.5.4" 8 | }, 9 | "scripts": { 10 | "server": "node pt.1-jQuery-to-React/javascript/server.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "space-before-function-paren": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/fbjs/.* 3 | 4 | [options] 5 | module.file_ext=.js 6 | module.file_ext=.json 7 | 8 | esproposal.class_static_fields=enable 9 | esproposal.class_instance_fields=enable 10 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "flow.pathToFlow": "node_modules/.bin/flow" 3 | } 4 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/NOTES.md: -------------------------------------------------------------------------------- 1 | # Poznámky k výkladu 2 | 3 | ## Příprava 4 | ### Flow 5 | Flow je super nástroj na kontrolu kódu. Pokud si nemůžete dovolit rozjet Typescript nebo nemůžete mít pořádný jazyk na psaní UI, třeba Elm, 6 | tak je **nutné** použít Flow, alespoň v módu `weak`. 7 | 8 | ### ESLint 9 | Bez ESLintu si nedovedu představit vývoj. Jasně, kdo má IDE, tak ho nepotřebuje. Ale ESLint má možnost zdědit konfigurace, které používají vývojáři ve světě jako standard. Například AirBnB vytvořilo svojí sadu pravidel, která je populární. Nebo tenhle repozitář používá [Standard.js](http://standardjs.com/rules.html) nastavení. ESLint krom kontroly hezkosti kódu dovede ještě kontrolovat typické chyby jako neexistující proměnné atp. 10 | 11 | ## Implementace 12 | 13 | ### jQuery špagety 14 | [kód je zde](./jQuery/imperative/index.html) 15 | 16 | - výsledek má být ukázat, že je šílený pořád jenom kopírovat komponenty a psát k nim trošku jinej kód kvůli jinému HTML 17 | 18 | - vysvětlit proč jsou v tom kódu ty spany - mají být naplněny textem 19 | 20 | - nechat první button jako danger a ukázat, jak je těžké tam tu původní classu smazat, že je lepší to celé vyresetovat 21 | 22 | - naimplementovat tlačítko reset 23 | 24 | - naimplementit defaultně otevřený dropdown podle checkboxu 25 | 26 | - Dropdown - zmínit, že unfold je naprosto v pohodě 27 | 28 | - Dropdown naiplmenentovat přidávání dalších options -> problém s tím, že předlohy mohou být prázdné 29 | - Ukázat problém se synchronizací - tak pohřbít jQuery 30 | 31 | - Dropdown - ukázat sečteno, kde se templaty používaj 32 | 33 | - idiocie toho, že mám použitý selectory, které jsou v HTML, ale JS mám v jinym souboru - tady se hezky ukáže seperation of concerns not technologies. 34 | 35 | - tohle je totálně netestovatelný, leda by se mockovat celej dom a psali se query selectory 36 | 37 | - nedá se procházet mezi stavy 38 | 39 | ### Templaty 40 | - best practices https://code.tutsplus.com/tutorials/best-practices-when-working-with-javascript-templates--net-28364 41 | - zlepšení v tom, že data už chodí jako JS objekty 42 | - problém je v tom, že pořád se někdě vkládá html do DOMu 43 | - NAIMPLEMENTOVAT - že celý řádek by byla šablona 44 | - ukázat problém s rekurzí u formulářových prvků 45 | - problém s šablonou a používání `$.html(html)` je v tom, že se všechny elementy nahradí, takže se vyresetují inputy a ztratí focus 46 | - taky je jasný, že jakmile se něco napíše do inputu, kterej se okamžitě přerenderuje, tak zmizí, co sem do něj napsal - to ale je řešitelný pře submit button nebo nějaké chytré diffování - k tomu se dostaneme 47 | - šablony maj jednu velikou nevýhodu - není tam javascript, ale jenom tupý string který umí jenom ifovat a vypisovat, nejde tam: 48 | - `typeof` a další normální funkce chybí (implementace od implementace) 49 | - nemají interface 50 | - rekurze - nemůžu použít na definici formuláře, která je rekurzní 51 | - našeptávání 52 | - kontrola existence proměnných 53 | - kontrola typos 54 | - jsou pomalé 55 | - ... you name it.. 56 | - proto nejde naimplementovat jednoduše formulář, protože nemám `typeof` na kontrolu typu fieldu 57 | - nicméně šablony fungují poměrně hezky, až na to, že nemají pořádné API 58 | - výborné je to, že view se dá vydefinovat jako `view = template(data)` 59 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/imperative/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 |
13 |
14 |

Style guide

15 |
16 |
17 |
18 |
19 |

API

20 |
21 |
22 |

Výsledek

23 |
24 |
25 |
26 |
27 |
28 |

Button

29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | Vyzkoušej btn-success 42 | 43 |
44 |
45 |
46 | 47 | 48 | <button class="btn btn-danger text-target"> 49 | This is default text 50 | </button> 51 | 52 | 53 |
54 |
55 |
56 | 59 |
60 |
61 |
62 |
63 | 64 | 65 | 127 | 128 | 129 | 130 |
131 |
132 |
133 |

Jídla

134 |
135 |
136 |
137 |
138 |
    139 | 140 | 141 |
    142 |
    143 |
    144 | 145 |
    146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/imperative/index.js: -------------------------------------------------------------------------------- 1 | // @flow weak 2 | 3 | 4 | (function($) { 5 | // simple button 6 | function fillCode(result, code) { 7 | code.html($.trim(result.html())) 8 | } 9 | 10 | // text 11 | $(document).on('input', '#simple-button-text', function() { 12 | var value = $(this).val() 13 | 14 | $('.simple-button-result button').html(value) 15 | fillCode($('.simple-button-result'), $('.simple-button-row .code-example')) 16 | }) 17 | 18 | // class 19 | var prevValue = null 20 | $(document).on('input', '#simple-button-class', function() { 21 | var value = $(this).val() 22 | 23 | var btn = $('.simple-button-result button') 24 | btn.removeClass('btn-danger') 25 | btn.addClass(value) 26 | btn.removeClass(prevValue || '') 27 | 28 | prevValue = value 29 | fillCode($('.simple-button-result'), $('.simple-button-row .code-example')) 30 | }) 31 | 32 | // dropdown 33 | // dropdown text 34 | var dropdownBtn = $('.btn-dropdown-result button') 35 | var defaultText = dropdownBtn.html() 36 | $(document).on('input', '#btn-dropdown-text', function() { 37 | // hezké zdvojení 38 | var value = $(this).val() || defaultText 39 | dropdownBtn.html(value) 40 | // tady to musí vyplnit 41 | fillCode($('.btn-dropdown-result'), $('.dropdown-btn-row .code-example')) 42 | 43 | // i tady 44 | dropdownBtn.html(value) 45 | }) 46 | 47 | // dropping down 48 | var unfoldCheckbox = $('#btn-dropdown-dropping') 49 | var handleDropdownUnfold = function() { 50 | var checked = $(this).prop('checked') 51 | 52 | if (checked) { 53 | $('.btn-dropdown-result .dropdown-menu').show() 54 | } else { 55 | $('.btn-dropdown-result .dropdown-menu').hide() 56 | } 57 | 58 | fillCode($('.btn-dropdown-result'), $('.dropdown-btn-row .code-example')) 59 | } 60 | handleDropdownUnfold.call(unfoldCheckbox) 61 | $(document).on('change', '#btn-dropdown-dropping', handleDropdownUnfold) 62 | 63 | 64 | function addMeal(li) { 65 | var clLi = li.clone() 66 | var deleteSpan = $('x') 67 | // mazani jidel 68 | deleteSpan.on('click', function() { 69 | clLi.remove() 70 | 71 | // kdekoliv, kde se bude pracovat s jidly, tak tam musí být tenhle kod 72 | $('.btn-dropdown-result .dropdown-menu li').each(function() { 73 | if (clLi.data('id') === $(this).data('id')) { 74 | $(this.remove()) 75 | } 76 | }) 77 | }) 78 | 79 | clLi.append(deleteSpan) 80 | $('.meals-menu').append(clLi) 81 | } 82 | // přidávání do dropdown 83 | $(document).on('keypress', '#btn-dropdown-option', function(event) { 84 | if (event.keyCode === 13) { 85 | var li = $('.btn-dropdown-result .dropdown-menu li').last().clone() 86 | var id = Date.now() 87 | li.attr('data-id', id) 88 | li.find('a').html($(this).val()) 89 | $('.btn-dropdown-result .dropdown-menu').append(li) 90 | $(this).val('') 91 | 92 | // pridat jidlo 93 | 94 | addMeal(li) 95 | } 96 | // a ted to zkusit s prázdnym inputem 97 | }) 98 | 99 | // synchronizace 100 | 101 | 102 | })(window.jQuery) 103 | 104 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdev-js-evenings/react-workshop/31a0a48f1a5419003195abbb8dc19f90e88f4a62/pt.1-jQuery-to-React/jQuery/index.html -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/style.css: -------------------------------------------------------------------------------- 1 | .api-col { 2 | border-right: 1px solid #ccc; 3 | } 4 | 5 | .api-row { 6 | border-bottom: 1px solid #ccc; 7 | } 8 | 9 | xmp { 10 | margin: 0; 11 | } 12 | 13 | 14 | .template { 15 | display: none; 16 | } 17 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Document 10 | 11 | 12 |
    13 |
    14 |
    15 |

    Style guide

    16 |
    17 |
    18 |
    19 |
    20 |

    API

    21 |
    22 |
    23 |

    Výsledek

    24 |
    25 |
    26 |
    27 |
    28 |

    Button

    29 |
    30 |
    31 |
    32 |
    33 |
    34 | 42 |
    43 |
    44 |
    45 |
    46 | 47 | 50 | 51 | 52 | 53 | 54 |
    55 |
    56 |
    57 | 58 |
    59 |
    60 |
    61 | 62 | 124 |
    125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/template/index.js: -------------------------------------------------------------------------------- 1 | (function($, handlebars) { 2 | (function() { 3 | // simple btn 4 | function render(html, where) { 5 | where.html(html) 6 | return where 7 | } 8 | 9 | function update(template, vars) { 10 | return handlebars.compile(template)(vars) 11 | } 12 | 13 | var simpleButtonTpl = $('#simple-btn-template') 14 | var formTpl = $('#var-template').html() 15 | 16 | var props = simpleButtonTpl.data('props') 17 | 18 | function updateSimpleButton(props, context) { 19 | var simpleButtonCode = update(simpleButtonTpl.html(), context) 20 | render(simpleButtonCode, $('.simple-btn-row .code-example')) 21 | render(simpleButtonCode, $('.simple-btn-row .result')) 22 | } 23 | 24 | var vars = Object.keys(props).map(function(varName) { 25 | return { name: varName } 26 | }) 27 | 28 | var defaultComponentContext = { 29 | type: 'btn-success', 30 | text: 'default text' 31 | } 32 | render(update(formTpl, { vars: vars }), $('.simple-btn-row .api-var-content')) 33 | updateSimpleButton(props, defaultComponentContext) 34 | 35 | var lastConext = {} 36 | $(document).on('input', '.simple-btn-row .string-input', function(e) { 37 | var context = {} 38 | var varName = this.name 39 | var value = this.value 40 | context[varName] = value 41 | 42 | var nextContext = Object.assign(lastConext, context) 43 | updateSimpleButton(props, Object.assign({}, 44 | defaultComponentContext, 45 | nextContext 46 | )) 47 | }) 48 | })(); 49 | 50 | 51 | (function() { 52 | // dropdown 53 | function render(html) { 54 | $('.dropdown-btn-row').html(html) 55 | } 56 | 57 | var dropdownBtnTemplate = handlebars.compile($('#dropdown-btn-template').html()) 58 | 59 | function update(template, context) { 60 | return template(context) 61 | } 62 | 63 | function renderApp(context, dropdownBtnTemplate) { 64 | render(update(dropdownBtnTemplate, context)) 65 | } 66 | 67 | var defaultContext = { 68 | vars: [ 69 | { name: 'text', type: 'string' }, 70 | { name: 'droped', type: 'boolean' }, // tenhle type nepude vyifovat v handlebars, leda přes helper http://stackoverflow.com/questions/24191182/how-to-check-type-of-object-in-handlebars 71 | { name: 'options', type: [{ name: 'text', type: 'string' }] } 72 | ], 73 | text: 'Default text', 74 | dropped: false 75 | } 76 | 77 | renderApp(defaultContext, dropdownBtnTemplate) 78 | 79 | var lastContext = {} 80 | // bum nepude tam psát! 81 | // mu se udělat submit button 82 | $(document).on('submit', '#vars-form', function(e) { 83 | e.preventDefault() 84 | var context = $(this).serializeArray().reduce(function(field, context) { 85 | context[field.name] = field.value 86 | return context 87 | }, {}) 88 | 89 | var nextContext = Object.assign(lastContext, context) 90 | renderApp(Object.assign({}, 91 | defaultContext, 92 | nextContext 93 | ), dropdownBtnTemplate) 94 | }) 95 | })() 96 | })(window.jQuery, window.Handlebars) 97 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/jQuery/template/jquery.index.js: -------------------------------------------------------------------------------- 1 | 2 | function getVars(txt) { 3 | var regex = /\{([^}]+)\}/g 4 | var results = [] 5 | var matches = regex.exec(txt) 6 | while (matches) { 7 | results.push(matches) 8 | matches = regex.exec(txt) 9 | } 10 | 11 | return results.reduce(function(vars, match) { 12 | var varName = match[1] 13 | if (!vars[varName]) { 14 | vars[varName] = [] 15 | } 16 | vars[varName].push(match['index']) 17 | return vars 18 | }, {}) 19 | } 20 | 21 | function replaceVars(string, variables) { 22 | var changes = [] 23 | Object.keys(variables).forEach(function(replaced) { 24 | var position = -1 25 | while ((position = string.indexOf(replaced, position + 1)) > -1) { 26 | if (!changes.hasOwnProperty(position)) { 27 | changes[position] = variables[replaced] 28 | if (replaced.length > 1) { 29 | var args = Array(replaced.length + 1) 30 | args[0] = position + 1 31 | args[1] = replaced.length - 1; 32 | [].splice.apply(changes, args) 33 | } 34 | } 35 | } 36 | }) 37 | 38 | var resultArray = string.split('').map(function(letter, index) { 39 | if (changes.hasOwnProperty(index)) { 40 | return changes[index] || '' 41 | } 42 | 43 | return letter 44 | }) 45 | 46 | return resultArray.join('') 47 | } 48 | 49 | (function($) { 50 | function applyVars(elem) { 51 | $(elem).closest('.row').each(function() { 52 | var vars = {} 53 | 54 | $('.var-template').each(function() { 55 | var val = $(this).find('input').val() 56 | var varName = $(this).data('varname') 57 | if (val) { 58 | vars['{' + varName + '}'] = val 59 | } 60 | }) 61 | 62 | var codeCol = $(this).find('.code-col code xmp.template') 63 | var resultCol = $(this).find('.result') 64 | var result = replaceVars(codeCol.html(), vars) 65 | resultCol.html(result) 66 | $(this).find('.code-col code xmp').not('.template').html(result) 67 | }) 68 | } 69 | 70 | $(function() { 71 | // handlovat change na políčkách pro proměnné (fakt náročný) 72 | $(document).on('input', '.var-template', function(e) { 73 | applyVars(this) 74 | // var val = e.target.value 75 | // var varName = $(this).data('varname') 76 | 77 | // // console.log('bum', val) 78 | // // console.log(codeCol.html().replace('{'+ varName +'}', val)) 79 | // var vars = {} 80 | // vars['{'+ varName +'}'] = val 81 | // var result = replaceVars(codeCol.html(), vars) 82 | // resultCol.html(result) 83 | // $(this).closest('.row').find('.code-col code xmp').not('.template').html(result) 84 | 85 | // ehm ehm 86 | // codeCol.html(replaceVars(codeCol.html(), vars)) 87 | }) 88 | 89 | $('.code-col code xmp').each(function() { 90 | var element = this 91 | $(element).parent().append($(element).clone().addClass('template')) 92 | $(this).closest('.row').find('.result').html($(this).html()) 93 | 94 | var html = $(this).html() 95 | // vyparsovat promměné z textu 96 | var vars = getVars(html) 97 | 98 | // vytvořit políčka pro proměnné 99 | Object.keys(vars).forEach(function(varName) { 100 | var cln = $(element).closest('.row').find('.var-template').first().clone() 101 | cln.removeClass('template') 102 | cln.find('label').text(varName) 103 | cln.attr('data-varname', varName) 104 | $(element).closest('.row').find('.api-var').append(cln) 105 | }) 106 | }) 107 | }) 108 | })(window.jQuery) 109 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/javascript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Document 10 | 11 | 12 |
    13 | 14 | 15 | 144 | 145 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/javascript/server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 |
    9 | 10 | 11 | 54 | 55 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/javascript/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | 4 | const app = express() 5 | const html = 'Server side!
    {HTML}
    ' 6 | 7 | class StringElement { 8 | constructor(tag, children = []) { 9 | this.tag = tag 10 | this.children = children 11 | } 12 | 13 | appendChild(child) { 14 | this.children.push(child) 15 | } 16 | 17 | attrsToString() { 18 | let stringAttrs = '' 19 | Object.keys(this).forEach(attr => { 20 | if (attr === 'tag' || attr === 'children') { 21 | return 22 | } 23 | 24 | stringAttrs += `${attr}="${this[attr]}"` 25 | }) 26 | 27 | return stringAttrs 28 | } 29 | 30 | toString() { 31 | return `<${this.tag} ${this.attrsToString()}>${this.children.map(child => child.toString()).join('')}` 32 | } 33 | } 34 | 35 | const serverDocument = { 36 | createElement(tag) { 37 | return new StringElement(tag) 38 | }, 39 | 40 | createTextNode(node) { 41 | return node 42 | } 43 | } 44 | 45 | function createDOM(document) { 46 | return ['div', 'span', 'label', 'p', 'h1', 'input', 'button', 'form'].reduce(function(DOM, tag) { 47 | DOM[tag] = function(attrs, children) { 48 | var elem = document.createElement(tag) 49 | elem = Object.assign(elem, attrs) 50 | 51 | if (typeof children === 'string') { 52 | elem.appendChild(document.createTextNode(children)) 53 | } else { 54 | children = children || [] 55 | children.forEach(function(child) { 56 | var child = typeof child === 'string' ? document.createTextNode(child) : child 57 | elem.appendChild(child) 58 | }) 59 | 60 | return elem 61 | } 62 | } 63 | 64 | return DOM 65 | }, {}) 66 | } 67 | 68 | var DOM = createDOM(serverDocument) 69 | 70 | function renderDOM(DOM, elem, fel) { 71 | elem.innerHTML = '' 72 | elem.appendChild(DOM) 73 | } 74 | 75 | 76 | function renderApp(state) { 77 | return DOM.span({}, [state.text]) 78 | } 79 | 80 | app.get('*', (req, res) => { 81 | res.send(html.replace('{HTML}', renderApp({ text: 'Jiný text' }))) 82 | }) 83 | 84 | app.listen(9999, () => { 85 | console.log('App is running on port:', 9999) 86 | }) 87 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-kurz", 3 | "version": "1.0.0", 4 | "description": "Kurz react pro WebDev", 5 | "main": "index.js", 6 | "author": "Vojta Tranta ", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "eslint": "^3.15.0", 10 | "eslint-config-standard": "^6.2.1", 11 | "eslint-plugin-promise": "^3.4.2", 12 | "eslint-plugin-standard": "^2.0.1", 13 | "flow": "^0.2.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/php/err.log: -------------------------------------------------------------------------------- 1 | [Thu Jan 19 22:40:54 2017] [crit] [client 127.0.0.1] configuration error: couldn't perform authentication. AuthType not set!: /index.php 2 | [Thu Jan 19 22:40:55 2017] [crit] [client 127.0.0.1] configuration error: couldn't perform authentication. AuthType not set!: /index.php 3 | [Thu Jan 19 22:40:56 2017] [crit] [client 127.0.0.1] configuration error: couldn't perform authentication. AuthType not set!: /index.php 4 | [Thu Jan 19 22:40:57 2017] [crit] [client 127.0.0.1] configuration error: couldn't perform authentication. AuthType not set!: /index.php 5 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/php/fn.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 |

    11 | 12 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/php/index.php: -------------------------------------------------------------------------------- 1 | 'Cau!'], './template.php')); 19 | */ 20 | -------------------------------------------------------------------------------- /pt.1-jQuery-to-React/php/template.php: -------------------------------------------------------------------------------- 1 | date('d.m.Y'), 8 | 'body' => 'This is body of an article', 9 | ], 10 | [ 11 | 'time' => date('d.m.Y'), 12 | 'body' => 'Another great article', 13 | ] 14 | ]; 15 | } 16 | 17 | $articles = getArticlesFromDatabase(); 18 | if (isset($_POST['body'])) { 19 | $articles[] = [ 20 | 'body' => $_POST['body'], 21 | 'time' => date('d.m.Y'), 22 | ]; 23 | } 24 | ?> 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Document 33 | 34 | 35 | 36 |

    37 | 38 |
    39 | 40 | 41 |
    42 | $article): ?> 43 |
    44 | 45 |

    46 | 47 |

    48 |
    49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pt.2-React/README.md: -------------------------------------------------------------------------------- 1 | ![Nautč se React.js](https://image.ibb.co/dnb7tv/react_event_fb_title_v03.png) 2 | 3 | # Naučte se React.js 1/3 - React pt.2 4 | Tak v minulé části jsme si vyvětlili a ukázali, proč není super nápad používat jQuery styl na velká UI. Nebyl to hejt na knihovnu jQuery, byl to hejt na styl, jakým se používá na tvorbu dynamického UI. 5 | 6 | Klasická HTML šablony implementované v Javascriptu se pro takový úkol hodí mnohem více, ale jejich problém tkví v tom, že jsou jenom parsovače stringu používající `String.replace()` a nic víc. Nepomáhají nám psát lepší a bezpečnější kód. 7 | 8 | Nicméně přístup vytváření HTML je cestou, kterou se vydat, daleko lépe se hodí pro tvoření velkého UI. 9 | 10 | Jako poslední jsme si ukázali styl, kdy píšeme celé UI v Javascriptových funkcích a tahle cesta vyřešila všechny problémy jQuery přístupu či javascriptových HTML šablon. 11 | 12 | Narazili jsme ale na neduhy a k jejich vyřešení potřebujeme splnit tyto body: 13 | 14 | - dokopat Javascript ke kontrole typů argumentů, chodících do šablon 15 | - zefektivnit vykreslování šablony - aby se upravilo jen to, co je potřeba a udržovaly se instance inputů, aby neztrácely focus apod. 16 | - přidat možnost psát šablony v přívětivějším duchu tak, aby kodéři neprskali 17 | 18 | Asi vás nepřekvapí, že všechny tyhle problémy vyřeší knihovna React za nás. Jdeme na to. 19 | ## Zázraky funkcí 20 | Ještě než se vrhneme na samotný React, tak bych se chtěl vrátit k našemu přístup s čistým Javascriptem. Slíbil jsem, že si ukážeme, jak jednoduché je si napsat serverové renderování, pokud máme čisté funkce. No a co jsem slíbil, to dodržím! 21 | 22 | Tak pokud se podíváte na kód [zde](../pt.1-jQuery-to-React/javascript/server.html) a spustíte si ho v prohlížeči, tak si všimnete prostě textu, který se vypíše jenom na klientu tj. HTML, které přichází ze serveru je prázdné, je tam pouze jeden `
    ` kam se renderuje aplikace. Renderování probíhá jenom na klientovi. 23 | 24 | No a když se podíváte [sem](../pt.1-jQuery-to-React/javascript/server.js) tak tady je kód pro spuštění klasického HTTP serveru v Node.js, který zatím nedělá nic. Spustíte si ho příkazem: 25 | ```bash 26 | yarn server 27 | ``` 28 | Na url [http://localhost:9999](http://localhost:9999) vám nyní běží ten zázrak. Vypíše se pouze nějaký text, nic zvláštního. 29 | 30 | No ale pokud vezmu náš známý kód, který přes objekt `DOM`, který přes metody `DOM.div(), DOM.span() ...` vytvářel DOM elementy a zkopírujete ho do `server.js` a změníte dvě řádky, tak uvidíte, jaké se budou dít věci... 31 | 32 | Stačí jenom upravit tyhle řádky: 33 | ```js 34 | var DOM = createDOM(document) 35 | // na: 36 | var DOM = createDOM(serverDocument) 37 | 38 | // a 39 | res.send(html.replace('{HTML}', 'SERVER')) 40 | // na: 41 | res.send(html.replace('{HTML}', renderApp(initialState))) 42 | ``` 43 | Restartujeme server a voilá! 44 | 45 | To bylo snadné, co? Zkuste si tohle udělat se nějakou Javascriptovou šablonou, jestli to pude tak snadno. 46 | 47 | Asi vás nepřekvapí, že téhle jednoduchosti využívá React a je schopný svoje šablony vyrenderovat i na Node.js serveru! 48 | 49 | To znamená, že SEO netrpí a aplikace nečeká na to, až stáhne Javascript, aby vůbec něco zobrazila, skvělé! 50 | 51 | ## Hurá na ten React 52 | Takže už opustíme do Reactu jako takového. 53 | 54 | ### Těžké začátky... 55 | Určitě jste zaregistrovali články jako slavné Javascript Fatique nebo vtípky na téma, že při hackathlonu se povedlo nainstalovat pouze Babel, takže se nikdo ani k Reactu nedostal. 56 | 57 | Jak to tak chodí, všechno to jsou takové polopravdy. React se dá použít stejným stylem jako jQuery, byť to opravdu je daleko od nějaké produkční konfigurace. 58 | 59 | Nicméně jsme skončili někde [zde](./basic.html). V tomhle příkladu renderujeme dvě úrovně nadpisu a k tomu ještě ukážeme `input`, který upravuje hlavní nadpis. Myšlenka byla taková, že při každém stisknutí klávesy se má aktualizovat nadpis podle toho, co je v inputu. To sice funguje, ale input ztrácí focus. Ten důvod je jasný, naše primitivní implementace není schopná udržet instance vyrenderované komponenty, takže ji pořád nahrazuje. 60 | 61 | Stejně tak mrháme výkonem, protože přerenderováváme celou aplikaci, ačkoli to je naprosto zbytečné, potřebovali bychom v DOMu aktualizovat jen ty kousky, které se změnili. 62 | 63 | Myslím, že tedy pohřbíme vlastní implementaci a vrhenem se na něco, co je už daleko víc vyvoněné a funkční no a to je React. 64 | 65 | #### Jak to rozjet 66 | Jak ho ale naroubujeme na naší implementaci DOMu? No, věřte tomu nebo ne, je potřeba do naší implementačky přidat nahoru jen pár řádek a pár jich smazat. 67 | 68 | V podstatě jde jenom o to nahradit: 69 | ```js 70 | var DOM = createDOM() 71 | 72 | // Reactí 73 | var DOM = React.DOM 74 | 75 | // a místo 76 | renderApp(dom, appElement) 77 | 78 | // použít funkci z Reactu 79 | ReactDOM.render(React.createElement(app, defaultData), appElement) 80 | ``` 81 | Abychom získali všechny globální proměnné, které jsou potřeba, stačí pouze jenom nakopčit do hlavičky klasickým stylem pár scriptů. Jsou to tyhle: 82 | ```html 83 | 84 | 85 | 86 | 87 | 88 | ``` 89 | No a pokud jste vše pečlivě nahradilim měla by se vám tedy v souboru [basic.html](./basic.html) zobrazit to samé, co bez Reactu. 90 | 91 | Takže co, je to teda těžké nebo ne? 92 | 93 | ### Co ten React vlastně je 94 | Trošku jsme se vykašlali na teoretickou část, tak co ten React vlastně je? 95 | 96 | No tak srovnání s jQuery je dost nefér, neboť jQuery je knihovna, která toho svede mnohem více než React. 97 | 98 | React totiž dělá jenom jednu věc a dělá ji poměrně dobře - šlo by to lépe. React je pouze knihovna pro tvorbu DOMu a jeho efektní modifikaci. Nic víc ni míň. 99 | 100 | Reactem tedy sám o sobě neumí: 101 | - request na server 102 | - parsování cookies 103 | - routování 104 | - modelovat logiku aplikace 105 | 106 | React se tedy soustředí na jednu jedinou část v procesu psaní SPA aplikace a to je view - DOM a jeho chytré aktualizování. Proto se React nedá srovnávat ani s Angularem. Neboť Angular je programovací rámec aplikace - framework, v podstatě tu aplikaci píšete "v mezích" nebo "uvnitř" Angularu. Kdežto React je opravdu jenom knihovna pro vypisování HTML. Pokud tedy budete chtít psát nějakou velkou aplikaci, tak se zřejmě neobejdete bez něčeho jako je Angular. 107 | 108 | Například v Avocode jsme si postavili vlastní "framework" byť si teda myslim, že ten náš "framework" je tak jednoduchý, že se o frameworku opravdu nedá mluvit. 109 | 110 | Samozřejmě okolo Reactu vyrostlo nějakolik přístupů jak tvořit aplikace a některé z nich jsou opravdu revoluční, třeba Flux arichtektura a její implementace Redux. Nebo úžasně jednoduché MobX. O těhle srandách se brzy dovíte. 111 | 112 | Teď ale bychom si měli říct, jak přemýšlet "React way" a tak si zkusíme napsat pár jednoduchostí. 113 | 114 | ### Zvláštnosti 115 | Opravdu divnost Reactu, která je taková no, jak to vyjádřit. Prostě každej si toho všimne a začne se tomu vysmívat, asi proto neexistuje výraz. Prostě něco, čeho si všimnete jako prvního, vysmějete se tomu, zavřete tab prohlížeče a napíšete o tom posměšný tweet. Tak taková věc je v Reactu JSX. 116 | 117 | Co to je? Toto: 118 | ```js 119 | const renderButton = (props) => { 120 | return ( 121 | 122 | ) 123 | } 124 | ``` 125 | Říkáte si WTF? Jakože HTML v Javascriptu? Tak je čas opustit přednášku, napsat posměšný tweet :)). 126 | 127 | Tohle má svoje důvody. 128 | 129 | Pamatujete si, jak jsme se bavili o tom, že psát UI, jehož výstupem je HTML v čistých javascriptových funkcích je prostě nečitelné a nepřehledné? To samé si řekli vývojáři Reactu a vymysleli tuhle syntax - JSX. 130 | 131 | V první řadě bych chtěl podtrhnout a skoro vykřiknout, že tohle **není** HTML. Neboť HTML je statický značkovací jazyk. Zatímce JSX v podstatě je Javascript. To, že napíšete: 132 | ```js 133 | return ( 134 | 135 | ) 136 | ``` 137 | Říkáte: Zavolej funkci `button` s argumetem `{ className: 'btn btn-success', children: 'Click me' }`. 138 | 139 | Nic víc!!! Navíc tenhle kód nejde ani spustit v prohlížeči (jde to, ale není to dobrý nápad dělat v produkci), tohle je čistě pseudosyntaxe, syntaxsugar nad Javascriptem. Nic víc nic míň. Bylo to vynalezeno právě proto, aby kodéři neprskali a měli se čeho chytit. Hlavně prosím prosím, neříkejte HTML v JavaScriptu nebo mi vybuchne hlava :)). 140 | 141 | Takže co se jako s timhle kódem stane, aby šel pustit v prohlížeči? 142 | 143 | Před: 144 | ```js 145 | const renderButton = (props) => { 146 | return ( 147 | 148 | ) 149 | } 150 | ``` 151 | Po kompilaci: 152 | ```js 153 | const renderButton = (props) => { 154 | return ( 155 | React.createElement('button', { 156 | 'className': "btn btn-success", 157 | 'children': 'Click me', 158 | }) 159 | ) 160 | } 161 | ``` 162 | Takže žádné HTML, jenom funkce, která vrací DOM element - Javascriptový objekt, žádný HTML string. 163 | 164 | Abychom si vyzkoušeli JSX, tak klidně můžeme použít `standalone babel transpiler` rovnou v prohlížeči. 165 | 166 | Tohle je jenom na hraní! Ten transpiler je obrovský, takže to v produkci nemá co dělat. Pro produkci je nejlepší si kód prostě hezky zkompilovat a připravit, né to cpát prasácky jako inline skripty bez explicitní závislosti!!! 167 | 168 | Ukázku JSX přímo v prohlížeči si můžete prohlédnout zde a rovnou začnem psát naší styleguidovou apku, tentokrát už zcela v Reactu. 169 | ### Základy React API 170 | #### Tvorba komponent a props 171 | React API je snadné. Můžete si vybrat, jestli psát komponenty jako funkce: 172 | ```js 173 | const renderButton = (props) => { 174 | return ( 175 | 176 | ) 177 | } 178 | ``` 179 | Nebo jako třídy. 180 | ```js 181 | class Button extends React.Component { 182 | render() { 183 | return ( 184 | 185 | ) 186 | } 187 | } 188 | ``` 189 | V případě třídy nám `props` tj. argumenty předané takto: 190 | ```js 191 | 192 | ``` 193 | Přicházejí jako `this.props`. 194 | Tudíž, zavoláme-li třídu `Button` takto: 195 | ```js 196 | const renderButton = (props) => { 197 | return ( 198 | 199 | ) 200 | } 201 | ``` 202 | Tak uvnitř této třídy budu mít dostupné: 203 | ```js 204 | this.props = { 205 | className: 'btn btn-success', 206 | onClick: naKlik, 207 | } 208 | ``` 209 | Takže důležitý pojem jsou `props`. To jsou parametry předané komponentě, v případě funkce je jasné že jsou jako první argument funkce. Props jsou vždy objekt. 210 | 211 | #### Stav komponent - state 212 | Pokud předáváte jenom props, tak nejste schopni nic měnit za běhu - aplikace prostě jenom vyrenderuje, nic víc, není možné nic změnit. 213 | 214 | Na změnu stavu komponenty používáme metodu `this.setState()`. 215 | 216 | Tahle metoda bere jako argument funcki nebo objekt. Jednodušší je předat objekt. 217 | 218 | `this.setState()` je možné logicky použít jenom u komponent, které jsou vytvořené jako třídy. Takže můžeme udělat třeba jednoduché počítadlo kliknutí. 219 | 220 | Zdrojá si prohlédněte [zde](./counter.html). 221 | ```html 222 | 247 | ``` 248 | Pozor, tady spouštíme `JSX` v prohlížeči - znovu opakuji, prohlížeč nedokáže spustit `JSX`. Proto musíme použít prasárnu a transpilovat přímo v prohlížeči, všimněte si proto ` 251 | ``` 252 | Nepoužíváme `text/javascript` ale `text/babel` a k tomu ještě divoká nastavení přes `data-presets`. Zatím netřeba řešit proč to tak je, stačí zkopčit a jet. Nezapomeňte na všechny skript z hlavičky!! Jinak vám to nepojede. 253 | 254 | Tak tohle jsou opravdové základy Reactího API, je toho víc. Zbytek se dozvíte od Petra Brzka a Jirky Vyhnálka. 255 | 256 | Teď ale k naší styleguidové apce. 257 | ### Create React app 258 | Je opravdu blbej nápad používat všechny skripty inline a transpilovat babelem přímo v prohlížeči, je to pomalé a stejně to v produkci nikdy nepoužijete, leda byste byli... No... Ale radši nechci ani řikat kdo.. :)) 259 | 260 | V repu jsem připravil takovou ultimátní hračičku, která se jmenuje `CRAP` neboli `create-react-app`. To je opravdu ta nejjednodušší cesta jak rozjet apku v Reactu. No a pak se můžete smát těm haterům, kteří nikdy nerozjeli Babel :). 261 | 262 | Takže stačí napsat v rooto tohodle repa. (samozřejmě po instalaci NPM balíčků) 263 | ```bash 264 | node_modules/.bin/create-react-app moje-aplikacka 265 | ``` 266 | Co to udělá? No prostě to vytvoří složku `moje-aplikacka` a když se pak do ní nastavíte, tak můžete jednoduše spustit 267 | ```bash 268 | npm start 269 | ``` 270 | A rozběhne se vám reactí apka se všema možnejma vychytávkama, aniž byste hnuli prstem... Zdrojáky jsou samozřejmě hezky dostupné v `moje-aplikacka/src` a jakmile je upravíte a uložíte, tak se stránka refreshne bez práce. 271 | 272 | Navíc máte k dispozici JSX a vůbec všechno a pěkně to funguje. Takže si jdeme hrát! 273 | 274 | Přepíšeme Aspoň dvě styleguidové komponenty do pravých reactích komponent! Jdeme na to! 275 | 276 | Inspirace je [zde](./index.html). 277 | -------------------------------------------------------------------------------- /pt.2-React/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Javascript only 14 | 15 | 16 |
    17 | 18 | 19 | 76 | 77 | -------------------------------------------------------------------------------- /pt.2-React/counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /pt.2-React/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /pt.2-React/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /pt.3-React-prakticky/README.md: -------------------------------------------------------------------------------- 1 | # React prakticky 2 | 3 | ## Teoretická část a prezentace 4 | 5 | 6 | 7 | 8 | 9 | **[Prezentace React prakticky](https://docs.google.com/presentation/d/1yuErzAASiDOWwvsODUVOPF1E8Xb6ji8LBqbcVSLp2zw/edit?usp=sharing)** 10 | 11 | 12 | ## Praktická část 13 | 14 | V praktické části práce jsme vyvíjeli aplikaci pro vytváření flashcards. Zdrojový kód a funkční specifikaci appky najdete v samostatném repozitáři [react-workshop-flashcards](https://github.com/webdev-js-evenings/react-workshop-flashcards). 15 | 16 | ## Zajímavé odkazy týkající se Reactu 17 | 18 | - [React patterns, techniques, tips and tricks](https://github.com/vasanthk/react-bits) 19 | - [JS.coach - vyhledávání React balíčků](https://js.coach/react) 20 | - [Awesome React - vyčerpávající list všeho skvělého pro React](https://github.com/enaqx/awesome-react) 21 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "semi": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/README.md: -------------------------------------------------------------------------------- 1 | ![Nautč se React.js](https://image.ibb.co/dnb7tv/react_event_fb_title_v03.png) 2 | 3 | # Naučte se React.js 5 - na Redux 4 | V téhle části si **pomalu** vysvětlíme proč se používá v React to, co se používá a proč to né zcela pasuje na možné známější backendové aplikace. 5 | 6 | Prostě si opravdu napíšeme vlastní plnohodnotný balíček pro práci se stavem, vlastně takový Redux. 7 | 8 | OK, né zas tak rychle. V první řadě si je třeba ukázat v čem je problém. 9 | 10 | S Petrem jsme si konečně napsali docela pěknou aplikačku, která vám třeba pomůže v učení se té hrozné haldy pojmů okolo Javascriptu, že? 11 | 12 | Takže se dá říct, že práce s komponentami už tak nějak zvládáme a můžeme se vrhnout na to trošku komplikovanější a to je konečně celá aplikace. 13 | 14 | ## Co je to aplikace 15 | Každá aplikace se skládá ze tří částí, které můžou mít různé názvy, ale v zásadě se ustálilo označení: 16 | * M - model 17 | * V - view 18 | * C - controller 19 | --------------- 20 | * S - service 21 | 22 | Věřte tomu nebo ne. Ekosystém okolo Reactu též ctí toto uspořádání, ačkoli se dá hovořit o nějakém to [CQRS](https://martinfowler.com/bliki/CQRS.html) nebo co to je. 23 | Každopádně bych se rát pohyboval v přátelštějších prostředí než se koukat do nějakého šíleného computer science. 24 | 25 | Pod čáru jsem ještě přidal zmínku o `Service`. Prostě servisy. Takový typ "třídy" se v aplikacích též hojně vyskytuje a má celou řadu vyjímečností... 26 | 27 | ### Jak se co nazývá v Reactu 28 | Když jsme psali Reactí komponenty, tak bylo zvykem mít tu velkou root komponentu naplněnou stave `state`. V takovém případě takováto komponenta vlastně 29 | zastává práci modelu a zároveň i kontroloru. Může dokonce zastávat i funkci view, pokud celá šablona bude vypsána v téhle jedné komponentně. 30 | To ale jistě není rozumnější. 31 | 32 | Takže narvat si tři části aplikaci do jedné "šablony" je samozřejmě zvláštní nápad, protože jasně mícháme něco, co má zcela jiné odpovědnosti, že? Takže se pustíme do rozsekávání nějaké masivní komponenty plné stavu. 33 | 34 | Kde začít? Nejsnažší je pro začátek vzít veklkou komponentu, která vykresluje všechno a udělat z ní "kontroler" ve terminologie Reactu z ní můžeme udělat Root komponentu nebo "chytrou" komponentu, která drží handlery callbacků a stav. 35 | 36 | Takže můžeme z ní udělat třeba něco takovéhleho: 37 | ```js 38 | export default class StatefulComponent extends Component { 39 | state = { 40 | todos: [ 41 | { id: 1, text: 'Nákup pro maminku na SváTek maTek' }, 42 | { id: 2, text: 'Nakoupit na víkendovou kalbu 2 kila lososa rozpejkací bagetky' }, 43 | ], 44 | user: { 45 | name: 'Vojta', 46 | email: 'vojta.tranta@gmail.com', 47 | id: 666, 48 | }, 49 | time: Date.now(), 50 | formValue: '', 51 | } 52 | 53 | _handleTick = (time) => { 54 | this.setState({ 55 | time, 56 | }) 57 | } 58 | 59 | _handleFormChange = (nextValue) => { 60 | this.setState({ 61 | formValue: nextValue, 62 | }) 63 | } 64 | 65 | _handleFormSubmit = () => { 66 | this.setState({ 67 | todos: [{ 68 | 'id': Date.now(), 69 | text: this.state.formValue, 70 | }].concat(this.state.todos), 71 | formValue: '', 72 | }) 73 | } 74 | 75 | render() { 76 | return ( 77 |
    78 |
    79 | logo 80 |

    Webdev React!!

    81 | 82 |
    83 |

    Stateful component

    84 |

    A list of todos

    85 |
    89 |
      90 | {this.state.todos.map(todo => { 91 | return 92 | })} 93 |
    94 |
    95 | ); 96 | } 97 | } 98 | 99 | ``` 100 | Je to jednoduché, místo toho, aby komponenta renderovala všechno, tak si vybere jen pár elementů a pak je vypíše, předá jim správná props a povídá si s nimi přes callbacky v props. 101 | 102 | Takováto architektura může připomínat klasický `Presenter` nebo `Controller`, který můžete znát z jiných jazyků či frameworků: 103 | ```php 104 | class Controller extends BaseController { 105 | function __construct($todoFacade, $context) { 106 | $this->todoFacade = $todoFacade; 107 | $this->context = $context; 108 | } 109 | 110 | public function index($request) { 111 | return $this->_createResponse($request, 'templates/index', [ 112 | 'page_title' => 'Index page!', 113 | 'todos' => $this->todoFacade->getTodos($request.get('page')), 114 | ]); 115 | } 116 | } 117 | ``` 118 | Tohle je příklad v PHP. Tak nějak vypadá nějaký typický kontroler, který vykresluje indexovou stránku pomocí šablony, který je umístěná v `templates/index`. Data do ní cpe z nějaké databázové fasády `TodoFacade`. 119 | 120 | Podobně funguje i rootová reaktí komponenta. Sebere data a narve je do šablony. Šablony jsou v tomto případě nějaké nižší jednodušší komponety, které z pravidla nedrží stav a jen využívají props. 121 | 122 | Tedy závěrem můžeme jednoduše říci, že "chytrá" komponenta tedy taková, která má stav a realizuje callbacky z potomků a rozdává do nich props je vlastně takový controller tj. v MVC zaujímá písmenko C. 123 | 124 | Hloupé komponenty jsou naproti tomu takové, která pouze přímají props a přes callbacky si povídají se svými předky. Předkům jsou tudíž plně podrobeny. Můžeme o nich tedy říct, že jsou to views, tedy V v MVC struktuře. 125 | 126 | ### A co "M"? 127 | M je tedy model, jak se řeší modelová vrstva v reactových aplikacích? 128 | 129 | Pomůžeme si zase příkladem ze známého světa PHP. Máme-li formulář, kde je například vypsán uživatelův profil, tak tento formulář často odpovídá jedné řádce tabulce v databázi, která se typicky jmenuje `user` nebo `user_profile`. Jakmile zde upravíme nějaká data, a odešleme formulář, tak se celá stránka překreslí a my vidíme nový stav uživatelova profilu, jasné jako facka? 130 | 131 | Tohle v SPA aplikacích ale samozřejmě nechceme. Proč bychom kvůli změně jednoho políčk překreslovali celou stránku, když by stačilo změnit pouze jednu hodnotu inputu, že. 132 | 133 | Jak to tedy udělat. Budeme tedy mít nějakou instanci modelu `User` a všechny komponenty na ní budou poslouchat, jak se mění, že? 134 | 135 | No, tak to úplně není. Každý model by musel být v tom případě nějaký event emitter a museli bychom ručně registrovat nebo odregistrovávat posluchače na tuto jednu instanci, která se navíc může v průběhu času měnit. 136 | 137 | To by bylo trošku šílené. 138 | 139 | Samotnout ideu modelu už v sobě mají Reactí komponenty zabudovanou. Je to property `state` a metoda `setState()`. 140 | 141 | Zajímavé na tom je, že React se nesnaží vytváře nějaký Event emitter, vůbec se s ním v Reactových komonentách nesetkáte. 142 | 143 | Pokud chcete změnit stav, prostě zavoláte `setState()` a to je všechno. Komponenta se sama překreslí a tím pádem aktualizuje i všechny svoje "děti", které vykresluje v metodě render. 144 | 145 | Samozřejmě tohle není ideální a perfektní řešení z hlediska výkonu, ale z hlediska jednoduchosti není co vytknout, potřebuju-li změnit stav, tak to prostě udělám. 146 | 147 | Model je tedy vyřešený? 148 | 149 | ### View != Model 150 | Už staří Egypťané ve svých hyeroglifech varovali před tímto: 151 | ```php 152 | 156 | 157 | 158 | 161 | 162 | 163 | 164 | $value): ?> 165 | 166 | 167 | 168 | 169 | 170 |
    171 | ``` 172 | Cítíte tu zrůdnost v tomhletom? 173 | 174 | No zkrátka, není správně dávat logiku (logika != Javascript) - myšleno to, jak se data tahají apod, a view (šablony) na stejné místo. 175 | 176 | Tedy přichází na scénu separation of concerns. React je knihovna pro tvorbu UI, není to knihovna pro správu dat. 177 | 178 | Jen abychom si rozuměli. Není špatné mít pár klíču ve `state` aplikace, ale je špatně mít tam stav celé aplikace - pokud už je aplikace velká. Stejně tak není rozumné mít v root komponentě všechny metody pro jeho změnu. Například není důvod, aby proces přihlášení uživatele byl v komponentě `Homepage`, může být v `LoginForm`, ale tam zase není radno dávat nějaký http requesty a podobně. 179 | 180 | Věcím je třeba dát řád podle toho, čeho se týkají a nemotat jablka a hrušky nebo skončíte u PHP a tam už jsme byli... 181 | 182 | ## Model? Stav! 183 | React tak nějak přišel s myšlenkou, že bychom se na aplikace měli koukat čistě jako na funkci stavu: 184 | ``` 185 | aplikace = šablona(stav) 186 | ``` 187 | A to je vše, nic víc není potřeba. 188 | 189 | A co je stav? No stav je pouze soubor klíčů je to v javascriptové terminologii objekt, který má klíče různých typů, u naší komponenty to vypadá takto: 190 | ```js 191 | state = { 192 | todos: [ 193 | { id: 1, text: 'Nákup pro maminku na SváTek maTek' }, 194 | { id: 2, text: 'Nakoupit na víkendovou kalbu 2 kila lososa rozpejkací bagetky' }, 195 | ], 196 | user: { 197 | name: 'Vojta', 198 | email: 'vojta.tranta@gmail.com', 199 | id: 666, 200 | }, 201 | time: Date.now(), 202 | formValue: '', 203 | } 204 | ``` 205 | A to je všechno, celá aplikace. Nic víc není potřeba k jejímu zobrazení, všechny informace jsou zde obsažené. 206 | 207 | Tedy stav je vlastně objekt a proč bychom ho museli mít přímo v jedné komonentě, nešlo by ho prostě vytáhnout ven? 208 | 209 | O co přijdem, pokud to uděláme? No přijdeme o možnost ho měnit přes `setState()` tak ho tedy necháme nadále obaleného v komponentě. Jaké budou benefity? 210 | 211 | No můžeme vytvořit dekorátor: 212 | ```js 213 | const createApp = (AppComponent) => (state) => { 214 | return class extends React.PureComponent { 215 | state = state 216 | 217 | _updatState = (stateUpdate) => { 218 | this.setState(stateUpdate) 219 | } 220 | 221 | render() { 222 | const props = { 223 | ...this.state, 224 | updateState: this._updatState, 225 | } 226 | 227 | return 228 | } 229 | } 230 | } 231 | ``` 232 | Dekorát je pouze funkce, která bere Reaktí komponentu jako argument a vrací funkci, která očekává stav jako argument a následně vrátí novou Reactí komopnentu, která je vlastně chytrá komponenta, která je ale už naprosto generická. Výhoda je taková, že už v žádné další komponentně nebude muset být schyzma mezi props a state, vše jsou prostě props. 233 | 234 | Použití takovéhoto dekorátoru pak vypadá následovně: 235 | ```js 236 | export default createApp(StateLessComponent)({ 237 | todos: [ 238 | { id: 1, text: 'Nákup pro maminku na SváTek maTek' }, 239 | { id: 2, text: 'Nakoupit na víkendovou kalbu 2 kila lososa rozpejkací bagetky' }, 240 | ], 241 | user: { 242 | name: 'Vojta', 243 | email: 'vojta.tranta@gmail.com', 244 | id: 666, 245 | }, 246 | time: Date.now(), 247 | formValue: '', 248 | }) 249 | ``` 250 | Výsledek této funkce je Reactí komponenta, která předala do `StateLessComponent` celý zde nadefinovaný stav a k tomu ještě funkci `updateState`, která je vlastně `setState()` a to je vše. 251 | 252 | Takže aplikace funguje dál, jen stačí všude smazat `this.state` a nahradit ho za `props` a `this.setState()` můžeme nahradit za prostou funkci `updateState()`. 253 | 254 | Výsledná původní `StatefulComponent` je nyní mnohem jednodušší a přejmenovaná na `StatelessComponent`: 255 | ```js 256 | 257 | const StateLessComponent = ({ ...state, updateState }) => { 258 | const _handleTick = (time) => { 259 | updateState({ 260 | time, 261 | }) 262 | } 263 | 264 | const _handleFormChange = (nextValue) => { 265 | updateState({ 266 | formValue: nextValue, 267 | }) 268 | } 269 | 270 | const _handleFormSubmit = () => { 271 | updateState({ 272 | todos: [{ 273 | 'id': Date.now(), 274 | text: state.formValue, 275 | }].concat(state.todos), 276 | formValue: '', 277 | }) 278 | } 279 | 280 | return ( 281 |
    282 |
    283 | logo 284 |

    Webdev React!!

    285 | 286 |
    287 |

    Stateful component

    288 |

    A list of todos

    289 | 293 |
      294 | {state.todos.map(todo => { 295 | return 296 | })} 297 |
    298 |
    299 | ) 300 | } 301 | ``` 302 | Ok, takže stav máme mimo komponentu, teď ještě jeho změny. 303 | 304 | ### Sémantické a nesémantické callbacky 305 | Asi jsem jediný na světě, kdo této záležitosti dal tak debilní jméno. Jaké záležitosti? 306 | 307 | Podívejme se na tyto dvě komponenty: 308 | ```js 309 | 310 | const Form = ({ children, onSubmit }) => ( 311 | 312 | {children} 313 | 314 | 315 | ) 316 | 317 | const UserForm = ({ onRegisterUserReqest }) => { 318 | let input 319 | const _handleFormSubmit = () => { 320 | const userName = input.value 321 | onRegisterUserReqest(userName) 322 | } 323 | 324 | return ( 325 |
    326 |

    Registration:

    327 |
    328 | 329 | input = userNameInput} /> 330 |
    331 |
    332 | ) 333 | } 334 | ``` 335 | Co konkrétně tyto komponenty dělají není příliš podstatné. Jen si ale všimněme, že komponenta `Form` bere callback `onSubmit`. To je prostě obecný callback, pokaždý, když se formulář submitne, tak se tento callback zavolá a je jedno, jaká data jsou ve formuláři, jestli to je formulář pro přidání todo a nebo formulář pro přidání nového uživatele. 336 | 337 | Zatímco "chytřejší" komponent `UserForm` bere callback `onRegisterUserRequest`. Tenhle callback už podle názvu značí co se daty bude dít po jejich odeslání. Ale ve své podstatě je to prostě jenom další `onSubmit` callback a nic víc. 338 | 339 | Já tedy takovýmto callbackům říkám `sémantické` neboť mi v aplikaci řeší nějkaou byznys logiku. Zatímco `nesémantické` callbakcy jako třeba `onSubmit`, `onClick`, `onFocus` mi neříkají nic o tom, co se má stát, když se zavolají, prostě bylo na něco kliknuto nebo něco bylo submitnuto, nic víc. 340 | 341 | Důvod, proč tyhle callbacky odlišuji je ten, že `sémantické` callbacky by se měly vyskytovat jen na úrovni `sémantické` komponenty. Zatímco jejím hloupým dětem (ahoj mami) by mělo bejt šumafuk, co se děje s daty, která odesílají či přijímají. 342 | 343 | A co je sémantická komponenta? Však to je jednoduché. 344 | Tohle je například nesémantická komponenta: 345 | ```js 346 | const ListItem = ({ title, body, children, id, onClick }) => ( 347 |
  • 348 | #{id} 349 | {title}
    350 |

    body

    351 | {children} 352 |
  • 353 | ) 354 | ``` 355 | A tohle je sémantická: 356 | ```js 357 | const TodoItem = ({ todo, onTodoClick }) => { 358 | return ( 359 | onTodoClick(todo)} 364 | > 365 | Created on {todo.created} 366 | 367 | ) 368 | } 369 | ``` 370 | Je patrný rozdíl mezi těmito komponentami? Jedna pracuje s obecnými daty, druhá, ta sémantická, renderuje konkrétní data do požadované podoby a případně usměrňuje callbacky a vrací do nich přínosná data, napříkald do `onTodoClick` vrátí celý objekt `todočka`, zatímco nesémantická komponenta by prostě jenom vrátila klasický `onClick` s argumentem `e: Event`. 371 | 372 | ### Proč to vyprávím? 373 | Je důležité si totiž uvědomit, kde končí "chytrý" kód, který implementuje business logiku a tudíž není univerzální a kód, který už je obecný, znovupoužitelný. To vám umožní zobecňovat koncepty a ušetřit si práci a aplikaci zjednodušit. 374 | 375 | Co je ale mnohem důležitější je to, že tyhle sématické callbacky dělají vaší aplikací smysluplnou. Proto jsem psal, že tyhle sémantické callbacky definují business logiku a jejich propojení se stav je skutečné jádro aplikace. Není, není to stav aplikace jako takový, ale **cesta jakou se stav mění**. 376 | 377 | To je teď největší slabina naší aplikace, změnu stavu je totiž definována pouze jako sémantický callback v "chytré" komponentě, který ačkoli je umístěný v komponentě přesně ví o tom, jak se má updatovat state, ačkoli by mu to mělo být putna. Zaměříme se tedy na refaktor téhle části kódu: 378 | ```js 379 | const _handleTick = (time) => { 380 | updateState({ 381 | time, 382 | }) 383 | } 384 | 385 | const _handleFormChange = (nextValue) => { 386 | updateState({ 387 | formValue: nextValue, 388 | }) 389 | } 390 | 391 | const _handleFormSubmit = () => { 392 | updateState({ 393 | todos: [{ 394 | 'id': Date.now(), 395 | text: state.formValue, 396 | }].concat(state.todos), 397 | formValue: '', 398 | }) 399 | } 400 | ``` 401 | ### Změna Je život 402 | No tak si představme, že tyhle callbacky v aplikace vůbec nejsou, to by ta aplikace byla pouze jenom statická stránka, ne? Takový PHPečko. Vlastně tyhle funkce jsou to, co dávají téhle aplikaci smysl a jsou proto naprosto esenciální. 403 | 404 | První úkol samozřejmě bude je dostat pryč z komponety tj. pryč z View. 405 | 406 | Jak na to? Hmm... 407 | 408 | Ideální by bylo, abychom dokázali volat z callbacků jenom funkce, která by nějak sami od sebe dokázali aktualizovat stav. 409 | 410 | To znamená, že bychom dostali funkce jako prop a tu bychom zavolali na nějaký callback a bylo by to. Ta funkce by měla mít také přístup ke stavu, neboť například aktualizace `todos` je možná pouze na základě původního listu. 411 | 412 | Bylo by tedy super prostě jenom aktualizovat stav například u hodin: 413 | ```js 414 | export default ({ time, setTime }) => ( 415 | 416 | ) 417 | ``` 418 | A funkce `setTime` by jenom vrace aktualizaci stavu 419 | ```js 420 | const setTime = (nextTime) => { 421 | return { 422 | time: nextTime, 423 | } 424 | } 425 | ``` 426 | Hmmm, ale jak toho docílit? No... To je lehounce komplikované, budeme tuto funkci muset prohnat přes náš dekorátor a konteinerovou komponetu držící stav, takže upravíme dekorátor: 427 | ```js 428 | 429 | const createApp = (AppComponent) => (state, actions) => { 430 | return class extends React.PureComponent { 431 | state = state 432 | 433 | _updatState = (stateUpdate) => { 434 | this.setState(stateUpdate) 435 | } 436 | 437 | _createActions() { 438 | return Object.keys(actions).reduce((actualActions, actionName) => { 439 | actualActions[actionName] = (...args) => { 440 | return this._updatState(actions[actionName](...args, this.state)) 441 | } 442 | 443 | return actualActions 444 | }, {}) 445 | } 446 | 447 | render() { 448 | const props = { 449 | ...this.state, 450 | ...this._createActions(), 451 | } 452 | 453 | return 454 | } 455 | } 456 | } 457 | ``` 458 | Metoda `_createActions` obalí funkce, které jí předáme tj: `createApp(Component)({ ..state }, { setTime })` -> funkci `setTime` takovou funkcí, která pokud je zavolaná tak pouze předá argumenty funkci `setTime` a výsledek její práce použije na aktualizaci stavu. K tomu ještě do funkce `setTime` přidá jako poslední argument aktuální stav, aby bylo vše jasné. 459 | 460 | Voila! 461 | 462 | ## Čeho jsme docílili? 463 | Pěkně jsme si zcela oddělili modelovou vrstvu a view vrstu a znatelně jsme rozsekali Controller. Z toho zbyla jenom čistá funkce, která pouze jenom předává data tam a callbacky je vrací zpátky. 464 | 465 | Tohle je opravdu elegantní řešení a dá se povýšit ještě o úroveň výše no a to si povíme příště... 466 | 467 | ## Co bude příště 468 | Nyní máme stav celé komponenty "přichycený" pouze na hlavní první root komponentě. To není tak špatné, ale jak aplikace roste a přidáme například taby nebo stránky, tak zjistíme, že by se hodilo mít tento stav přichycený k jiným komponentám, například k jednotlivým stránkám. 469 | 470 | No a to si ukážeme příště, jak "koukat" na stav z více míst v aplikaci pomocí neviditelného `contextu`. 471 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pt.4", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "create-react-app": "^1.3.1", 7 | "false": "0.0.4", 8 | "global": "^4.3.2", 9 | "moment": "^2.18.1", 10 | "react": "^15.5.4", 11 | "react-dom": "^15.5.4" 12 | }, 13 | "devDependencies": { 14 | "react-scripts": "0.9.5" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdev-js-evenings/react-workshop/31a0a48f1a5419003195abbb8dc19f90e88f4a62/pt.4-React-sprava-stavu/public/favicon.ico -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
    20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | .list { 22 | width: 500px; 23 | margin: 1em auto; 24 | text-align: left; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { transform: rotate(0deg); } 29 | to { transform: rotate(360deg); } 30 | } 31 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/clock.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import moment from 'moment' 4 | 5 | 6 | export default class Clock extends React.Component { 7 | _timeout = null 8 | 9 | static defaultProps = { 10 | tickInterval: 'second', 11 | } 12 | 13 | componentDidMount() { 14 | this._timeout = setTimeout(this._handleTick, this.props.tick) 15 | } 16 | 17 | componentWillUnmount() { 18 | clearTimeout(this._timeout) 19 | } 20 | 21 | _handleTick = () => { 22 | this.props.onTick( 23 | moment(this.props.time) 24 | .add(this.props.tick / 1000, this.props.tickInterval) 25 | ) 26 | setTimeout(this._handleTick, this.props.tick) 27 | } 28 | 29 | render() { 30 | return ( 31 | {moment(this.props.time).format('H:mm:ss')} 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './just-component/just-component' 4 | import './index.css' 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ); 10 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/initial-state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | todos: [ 3 | { id: 1, text: 'Nákup pro maminku na SváTek maTek' }, 4 | { id: 2, text: 'Nakoupit na víkendovou kalbu 2 kila lososa rozpejkací bagetky' }, 5 | ], 6 | user: { 7 | name: 'Vojta', 8 | email: 'vojta.tranta@gmail.com', 9 | id: 666, 10 | }, 11 | time: Date.now(), 12 | formValue: '', 13 | } 14 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/just-component/just-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../logo.svg'; 3 | import '../App.css'; 4 | 5 | import Clock from '../clock' 6 | import Form from '../todo-form/form' 7 | import Todo from '../todo-form/todo' 8 | 9 | import initialState from '../initial-state' 10 | 11 | 12 | 13 | const createApp = (AppComponent) => (state, actions) => { 14 | return class extends React.PureComponent { 15 | state = state 16 | 17 | _updatState = (stateUpdate) => { 18 | this.setState(stateUpdate) 19 | } 20 | 21 | _createActions() { 22 | return Object.keys(actions).reduce((actualActions, actionName) => { 23 | actualActions[actionName] = (...args) => { 24 | return this._updatState(actions[actionName](...args, this.state)) 25 | } 26 | 27 | return actualActions 28 | }, {}) 29 | } 30 | 31 | render() { 32 | const props = { 33 | ...this.state, 34 | ...this._createActions(), 35 | } 36 | 37 | return 38 | } 39 | } 40 | } 41 | 42 | 43 | const StateLessComponent = ({ ...state, addTodo, setTime, setTodoDraft }) => { 44 | return ( 45 |
    46 |
    47 | logo 48 |

    Webdev React!!

    49 | 50 |
    51 |

    Just a Component

    52 |

    A list of todos

    53 |
    57 |
      58 | {state.todos.map(todo => { 59 | return 60 | })} 61 |
    62 |
    63 | ) 64 | } 65 | 66 | 67 | const setTime = (nextTime) => { 68 | return { 69 | time: nextTime, 70 | } 71 | } 72 | 73 | const setTodoDraft = (todoDraft) => { 74 | return { 75 | formValue: todoDraft, 76 | } 77 | } 78 | 79 | const addTodo = (event, state) => { 80 | return { 81 | todos: [{ 82 | 'id': Date.now(), 83 | text: state.formValue, 84 | }].concat(state.todos), 85 | formValue: '', 86 | } 87 | } 88 | 89 | 90 | export default createApp(StateLessComponent)(initialState, { 91 | addTodo, 92 | setTime, 93 | setTodoDraft, 94 | }) 95 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/stateful-component/stateful-component.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from '../logo.svg'; 3 | import '../App.css'; 4 | 5 | import Clock from '../clock' 6 | import Form from '../todo-form/form' 7 | import Todo from '../todo-form/todo' 8 | 9 | 10 | 11 | export default class StatefulComponent extends Component { 12 | state = { 13 | todos: [ 14 | { id: 1, text: 'Nákup pro maminku na SváTek maTek' }, 15 | { id: 2, text: 'Nakoupit na víkendovou kalbu 2 kila lososa rozpejkací bagetky' }, 16 | ], 17 | user: { 18 | name: 'Vojta', 19 | email: 'vojta.tranta@gmail.com', 20 | id: 666, 21 | }, 22 | time: Date.now(), 23 | formValue: '', 24 | } 25 | 26 | _handleTick = (time) => { 27 | this.setState({ 28 | time, 29 | }) 30 | } 31 | 32 | _handleFormChange = (nextValue) => { 33 | this.setState({ 34 | formValue: nextValue, 35 | }) 36 | } 37 | 38 | _handleFormSubmit = () => { 39 | this.setState({ 40 | todos: [{ 41 | 'id': Date.now(), 42 | text: this.state.formValue, 43 | }].concat(this.state.todos), 44 | formValue: '', 45 | }) 46 | } 47 | 48 | render() { 49 | return ( 50 |
    51 |
    52 | logo 53 |

    Webdev React!!

    54 | 55 |
    56 |

    Stateful component

    57 |

    A list of todos

    58 | 62 |
      63 | {this.state.todos.map(todo => { 64 | return 65 | })} 66 |
    67 |
    68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/stateless-component/business-logic.js: -------------------------------------------------------------------------------- 1 | export const setTime = (time) => { 2 | return { 3 | time, 4 | } 5 | } 6 | 7 | export const setFormValue = (nextValue) => { 8 | return { 9 | formValue: nextValue, 10 | } 11 | } 12 | 13 | export const addNewTodo = (formValue, todos) => { 14 | return { 15 | todos: [{ 16 | 'id': Date.now(), 17 | text: formValue, 18 | }].concat(todos), 19 | formValue: '', 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/stateless-component/stateless-component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../logo.svg'; 3 | import '../App.css'; 4 | 5 | import Clock from '../clock' 6 | import Form from '../todo-form/form' 7 | import Todo from '../todo-form/todo' 8 | 9 | 10 | 11 | const createApp = (AppComponent) => (state) => { 12 | return class extends React.PureComponent { 13 | state = state 14 | 15 | _updatState = (stateUpdate) => { 16 | this.setState(stateUpdate) 17 | } 18 | 19 | render() { 20 | const props = { 21 | ...this.state, 22 | updateState: this._updatState, 23 | } 24 | 25 | return 26 | } 27 | } 28 | } 29 | 30 | 31 | const StateLessComponent = ({ ...state, updateState }) => { 32 | const _handleTick = (time) => { 33 | updateState({ 34 | time, 35 | }) 36 | } 37 | 38 | const _handleFormChange = (nextValue) => { 39 | updateState({ 40 | formValue: nextValue, 41 | }) 42 | } 43 | 44 | const _handleFormSubmit = () => { 45 | updateState({ 46 | todos: [{ 47 | 'id': Date.now(), 48 | text: state.formValue, 49 | }].concat(state.todos), 50 | formValue: '', 51 | }) 52 | } 53 | 54 | return ( 55 |
    56 |
    57 | logo 58 |

    Webdev React!!

    59 | 60 |
    61 |

    Stateless component

    62 |

    A list of todos

    63 | 67 |
      68 | {state.todos.map(todo => { 69 | return 70 | })} 71 |
    72 |
    73 | ) 74 | } 75 | 76 | 77 | 78 | export default createApp(StateLessComponent)({ 79 | todos: [ 80 | { id: 1, text: 'Nákup pro maminku na SváTek maTek' }, 81 | { id: 2, text: 'Nakoupit na víkendovou kalbu 2 kila lososa rozpejkací bagetky' }, 82 | ], 83 | user: { 84 | name: 'Vojta', 85 | email: 'vojta.tranta@gmail.com', 86 | id: 666, 87 | }, 88 | time: Date.now(), 89 | formValue: '', 90 | }) 91 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/todo-form/form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | export default ({ value, onChange, onSubmit }) => { 5 | const handleFormSubmit = (e) => { 6 | e.preventDefault() 7 | onSubmit(e) 8 | } 9 | 10 | return ( 11 | 12 |
    13 | onChange(e.target.value)} 16 | />
    17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /pt.4-React-sprava-stavu/src/todo-form/todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | export default ({ todo, onClick }) => ( 5 |
  • onClick(todo)}> 6 | {todo.text} 7 |
  • 8 | ) 9 | -------------------------------------------------------------------------------- /pt.5-React-redux/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "semi": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pt.5-React-redux/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /pt.5-React-redux/README.md: -------------------------------------------------------------------------------- 1 | ![Nautč se React.js](https://image.ibb.co/dnb7tv/react_event_fb_title_v03.png) 2 | 3 | # Naučte se React.js 6 - Redux 4 | ## Minule 5 | [Minulý workshop](../pt.4-React-sprava-stavu) ukázal, jak si vytáhnout stav z komponenty "někam jinam". Neboť už Sumerové, jak známo, zaznamenávali stavy jejich aplikací mimo view vrstvu. Nebyli to prasata, aby psali SQL query a připojení do databází přímo do "HTML". 6 | 7 | Vytažení stavu z "živých" komponent přineslo výhody v tom, že s kód komponent zpřehlednil. Nebylo už třeba řešit `setState()`, nebylo už ani potřeba rozlišovat mezi `this.state` a `this.props`. Najednou se mohly všechny komponenty přepsat do pouhých funkcí. 8 | 9 | ## Dneska 10 | Tak to je všechno pěkný, ale má to pár much. Zdůrazňoval jsem, že jádro každé aplikace - její byznysová logika - leží ve změnách. Tedy v tom, jak data v průběhu času transformujeme. 11 | 12 | Hezky navržená relační databáze je sama o sobě k ničemu, pokud se do ní nedá zapisovat a údaje měnit a nebo je vytahovat a prezentovat. Tohle všechno je **transformace dat**. Tedy merit každá aplikace. 13 | 14 | Dnešní workshop se tedy bude točit okolo transformací dat. Hlavně bude o tom, jak se začít dívat na aplikace spíš jako na sekvence událostí než pouhé "obrazovky". 15 | 16 | Jdeme na to. 17 | 18 | ## Apka 19 | Dneska se teda budeme bavit vytvářením apky, která vám pomůže spočítat daně. Hurá, konečně něco opravdu užitečného, což? Zapneme si tam dokonce Firbasku, aby to bylo trošku srandovnější - ale uvidíme, jestli se k tomu vůbec dostaneme. V první řadě si navrhneme tvar stavu aplikace. 20 | 21 | 22 | ## Model 23 | Tedy modelová vrstvička aplikace. 24 | 25 | K výpočtu daně potřebujeme znát určitě příjem. Příjem se dá vypočítat ze sumy pohledávek neboli vydaných faktur. Takže určitě bude potřeba seznam faktur. 26 | 27 | Každá faktura bude mít číslo, odběratele, cenu, DPH a celkovou cenu. Klasika. 28 | 29 | Faktury by mělo být možné přidávat upravovat atd atd. To je jasné. 30 | 31 | Daňová kalkulačka si pak vždycky při změně faktur přepočítá všechny potřebné informace. To je samozřejmě pouze jenom příjem, který je potřeba pro výpočet daně. Ještě si k tomu vypočítáme DPHáčko, podle toho, jak zbyde čas. 32 | 33 | Takže stav bude vypadat nějak takhle: 34 | ```js 35 | const initialData = { 36 | vatRatio: 21, 37 | invoices: [{ 38 | 'id': '2017/1', 39 | 'customer': 'Avocode Inc.', 40 | 'price': 123000, 41 | 'VAT': 25830, 42 | 'total': 148830, 43 | }], 44 | tax: { 45 | income: 123000, 46 | costsRatio: 60, 47 | taxRatio: 15, 48 | tax: 7380, 49 | }, 50 | user: { 51 | username: '', 52 | email: '', 53 | id: 0, 54 | } 55 | } 56 | ``` 57 | 58 | Přihlášené user si tu zatím ponecháme a použijeme ho pro práci s Firebaskou. 59 | 60 | ## Akce! 61 | Tak ještě naposledy. To co utváří aplikace jsou **změny stavu**. To je po tvaru stavu to nejdůležitější v aplikaci. Proto je dobré o takových akcí mít vždy co největší podvědomí a vědět přesně, co dělají. Nebo dokonce co celá aplikace může dělat. 62 | 63 | V minulé lekci jsme neměli definováno nic jako "akce". Změnu stavu aplikace byla provedena buďto přes `this.setState()` na React komponentně. 64 | 65 | Nebo přes nějaký předaný callback přes props, který jsme si nějak sémanticky pojmenovali. Třeba `setTodoDraft()` nebo `addTodo()`. 66 | 67 | Tohle můžeme prohlásit za dostačující. Za docela hezkou architekturu. 68 | 69 | Jen drobné vylepšení můžeme poskytnout do naší aplikace tak, že přidáme nějakou jasnější informaci o tom, co se v aplikaci děje. Sice z názvu funkce `addTodo()` je jasné, že přidává další TODOčko, ale možná by nebylo od věci si akce pojmenovat nějakuo hezkou konstantou ve stringu třeba `ADD_TODO`. 70 | 71 | Tohle se může hodit například při debugování nebo při streamování akcí přes sockety do jiného počítače. Také můžeme pak poměrně jasně vidět, jaké akce jaká část aplikace využívá a můžeme si je sémanticky shlukovat. navíc nám to nedá tolik práce. Případné API si představuji takto: 72 | ```js 73 | const addTodo = action('ADD_TODO', (todoText) => { 74 | return { 75 | text: todoText, 76 | id: uuid.v4(), 77 | } 78 | }) => { 79 | action: 'ADD_TODO', 80 | payload: { 81 | text: todoText, 82 | id: id, 83 | } 84 | } 85 | ``` 86 | Prostě jenom funkci obalíme. 87 | 88 | Další prvek vylepšení funkcí by mohlo být oddělení toho, jak se vytváří akce (jestli přichází jen z UI nebo jestli přichází jako nějaká aktivita třeba ze sítě nebo z URL apod.). Pokud tohle chování oddělíme můžeme si pak všimnou toho, že se naše aplikace izoluje od okolního světa: 89 | 90 | ``` 91 | _________AKCE________ 92 | | | 93 | A A 94 | --Kliknutí---> K K <----------HTTP-------- 95 | --WebSocket--> C Naše apka C <-----history.push()--- 96 | E E 97 | | | 98 | ---------AKCE--------- 99 | ``` 100 | To sem hezky nakreslil! 101 | 102 | Akce se chovají jako hranice mezi okolním světem (prohlížeč, klávesnice, síť) a naší aplikací, bude to taková hradba, aby se do apky nedostával bordel zvenčí. 103 | 104 | Jediná věc, které bude rozumět naše aplikace budou jenom akcičky. To znamená, že bude vědět, jak zpracovat takovýto příkaz: 105 | ```js 106 | { 107 | action: 'ADD_TODO', 108 | payload: { 109 | 'todoText': 'Nová todo', 110 | 'id': '12323SDFSDFadf', 111 | } 112 | } 113 | ``` 114 | Ale nebude cesta, jak v apce něco "zavolat" nějakou metodu zvenčí. Jakákoliv komunikace s naší aplikací se bude muset přeložit do předem definované akce, které bude apka rozumět. 115 | 116 | Teď ale je otázka, jak bude apka akce zpracovávat. O to se postará Store. 117 | 118 | ## Store 119 | V předešlé lekci jako Store, neboli jako komponenta držící stav, nám postačila jen kmponenta. A posloužila opravdu dobře. Komponenty totiž sami od sebe umí vše překreslit při nějakém volání `setState()` takže nebylo potřeba vytváře nějaké listenery apod. 120 | 121 | Ale to, že stav byl je v komponentně sebou neslo tu nevýhodu, že byl pouze jenom celý přístupný jen v jedné "vrchní" chytré komponentě a né někde dole. 122 | 123 | Pokud si představíme nějaký strom komponent: 124 | ![Strom komponent](http://arqex.com/wp-content/uploads/2015/02/trees.png) 125 | 126 | Tak všude vidíme tenhle nevinný obrázek. No jo, ale většina aplikací je daleko, daleko komplexnějších a mají v sobě ohromné zanoření komponentového stromu. Pokud máte jenom dvě úrovně, tak máte miniaplikačku. Takové Avocode má takových úrovní fakt mraky a každá velká aplikaci na tom bude podobně. 127 | 128 | To znamená, že by bylo dost šílené si předávat všechny potřebané props pouze z jednoho místa až dolů někam totálně hluboko do stromu. Stalo by se tak, že by komponenty dostávaly props, které by jenom přeposílaly dál a to je znak trošku smrdutého kódu neboť není potřeba, aby lord prosil Jeana, aby mu přinesl klavír, protože na něm má doutník. Každý by měl dostat jen to, co potřebuje a tak je to i s Props. 129 | 130 | Takže jak se toho zbavit. 131 | 132 | No, naštěstí tu máme velkou obeličku a tou je React a jeho `context`. 133 | 134 | ### Context 135 | Pokud jste o Contextu v Reactu neslyšeli, tak to je takový maličký hack. Je to vlastně takový DI container zabudovaný v Reactu, který funguje pouze přes Stringy. Jeho použití je následující. 136 | 137 | Řekněme, že máme službu třeba `API`, které requestuje nějaké endpointy. Pokud byste se striktně držely Reactí filozofie, tak byste si museli předávat referenci na `API#get()` do stromu komponent až tam, kde je to potřeba. To je ale trošku šílené, ne? 138 | 139 | Proč takle radši APIčko Reactu nepředhodit, aby ho distribuoval, kde je potřeba: 140 | ```js 141 | class extends React.Component { 142 | static childContextTypes = { 143 | api: React.PropTypes.Object, 144 | } 145 | 146 | getChildContext() { 147 | return { 148 | api: new Api(new HttpRequestFactory()), 149 | } 150 | } 151 | 152 | render() { 153 | return ( 154 | // ta má dítě DiteHlavniKomponenty 155 | ) 156 | } 157 | } 158 | 159 | class DiteHlavniKomponenty extends React.Component { 160 | static contextTypes = { 161 | api: React.PropTypest.object.isRequired, 162 | // řekneme si o api v context, něco jako v /* @inject */ v Nette 163 | } 164 | 165 | render() { 166 | const api = this.context.api // Juhů takle je API dostupné a nic není potřeba předávat přes props. 167 | 168 | return (..) 169 | } 170 | } 171 | ``` 172 | Parádička. Samozřejmě zkušení architekti cítí, že je to prasárnička, ale to přejdeme. My si totiž ten kontext pečlivě schováme, abychom se o něj nemusely starat! 173 | 174 | ### Context provider 175 | Často můžete vidět kód: 176 | ```js 177 | render() { 178 | return ( 179 | 180 | 181 | 182 | ) 183 | } 184 | ``` 185 | Takle se hází do Reactího stromu komponent kontext. Takováhle `ApiContextProvider` komponenta pak uvnitř vypadá takto: 186 | ```js 187 | class ApiContextProvider extends React.PureComponet { 188 | static contextTypes = { 189 | api: React.PropTypes.object, 190 | } 191 | 192 | getChildContext() { 193 | return { 194 | api: this.props.api, 195 | } 196 | } 197 | 198 | render() { 199 | return this.props.children 200 | } 201 | } 202 | ``` 203 | Takováhle komponenta pouze vezme svoje props a hodí je do contextu, aby byly přístupny hluboko v DOMu bez nutnosti je předávat. 204 | 205 | Této vlastnosti využijeme, abychom už ale naprosto oddělili stav aplikace od Reactu a jeho API. 206 | 207 | #### Connect 208 | Connect bude dekorátor, kterým obalíme komponentu proto, aby byla schopná přijmout data z jednoho hlavní storu kdekoliv ve stromu komponent. Tedy ne už někde na vrcholu stromu, ale klidně někde hluboko ve stromu. API si předtavuji takto: 209 | ```js 210 | class ConnectedReactComponent extends React.Component { 211 | render() { 212 | console.log(this.props.invoicesFromTheStore) 213 | 214 | return (...) 215 | } 216 | } 217 | 218 | connect({ 219 | 'invoices': 'invoicesFromTheStore', // klíč - klíč ve stavu, hodnota - název klíče v props, který půjde do componenty 220 | }, actions)(ConnectedReactComponent) 221 | ``` 222 | Voila, elegantní, že? Samozřejmě komponenta se bude chovat porádně stejně jako každá jiná, jen bude trošku obohacena o pár klíčů ze Store. 223 | 224 | Druhý parametry funkce `connect()` bude objekt s akcemi, ty pak přijdou do komponenty jako props: 225 | ```js 226 | connnect(propsFromStore, { 227 | 'onInvoiceAdd': () => ({ // V komponentě pak bude tahle funkce přístupná jako props.onInvoiceAdd 228 | 'invoice': { // výseledek volání funkce bude přímu dispatchnut přes store 229 | price: 300, 230 | VAT: 21, 231 | } 232 | }) 233 | })(ConnectReactComponent) 234 | ``` 235 | 236 | 237 | ### Gimme da Store 238 | Takže store nebude nic jiného než event emitter, který bude mít referenci na stav a bude ho průběžně měnit: 239 | ```js 240 | class Store { 241 | _listeners = [] 242 | _state = {} 243 | 244 | constructor(initialState) { 245 | this._state = initialState 246 | } 247 | 248 | _emitChange() { 249 | this._listeners.forEach(listener => listener()) 250 | } 251 | 252 | listen(listener) { 253 | this._listeners.push(listener) 254 | } 255 | 256 | getState() { 257 | return this._state 258 | } 259 | } 260 | ``` 261 | Ok, super, jak ale budeme ten stav měnit? 262 | 263 | Na si vytvoříme metodu `dispatch()`, která převezme akci a aplikuje ji na stav... Hmmmm. Aplikuje akci na stav: 264 | ```js 265 | aplikujAkciNaStav(stav, akce) => nový stav 266 | ``` 267 | Tohle je hrozně jednoduchá myšlenka, na které se dá stavět. 268 | 269 | Přece naše aplikace jest pouze a jenom o změnách nějakého stavu. Stav máme nadefinovaný a změna stavu - no minimálně její jedna část je akce, pamatujete? To je prostě jendoduchý popis změny: 270 | ```js 271 | { 272 | action: 'ADD_TODO', 273 | payload: { 274 | 'todoText': 'Nová todo', 275 | 'id': '12323SDFSDFadf', 276 | } 277 | } 278 | ``` 279 | Takováhle akce říká: Moje drahá aplikace, proveď prosímtě změnu stavu, kterou jsme pojmenoval `ADD_TODO` tak, že přidáš nové todočka s textem `Nová todo` a idčkem `12323SDFSDFadf`. Postarej se o to. 280 | 281 | Dobře, takže máme vydefinovanou akci, teď ale jak aplikovat změnu. Potřebujeme totiž změni stav aplikace za pomoci výše vydefinované akce. Jak na to? 282 | 283 | Takže my chceme vzít jednu akci a jeden stav a vytvořit z ní jednu věc -> nový stav. 284 | 285 | To je vlastně redukování, vezmeme stav a akci a vrátíme změněný stav (nový stav). To je přece jasně zapsané tady: 286 | ```js 287 | aplikujAkciNaStav(stav, akce) => nový stav 288 | ``` 289 | Takže stejně tak naimplementujeme metodu dispatch... 290 | ```js 291 | dispatch({ action, payload }) { 292 | if (action === 'ADD_TODO') { 293 | this._state.todos.push({ 294 | text: payload.todoText, 295 | id: payload.id 296 | }) 297 | 298 | this._emitChange() 299 | } 300 | } 301 | ``` 302 | 303 | A je to... No.. Je to. Trošku tu lžu, tohle nevrací totiž nic. Co ale kdybychom si tu změnu předali přes konstruktor do Storu? 304 | 305 | A jak se ta změna jmenuje? No když něco `redukujeme`, tak se jedné o `reducer`: 306 | ```js 307 | class Store { 308 | _listeners = [] 309 | _state = {} 310 | _reducer = null 311 | 312 | constructor(initialState, reducer) { 313 | this._state = initialState 314 | this._reducer = reducer 315 | } 316 | 317 | //... 318 | } 319 | ``` 320 | A teď metoda dispatch lépe a hezčeji: 321 | ```js 322 | dispatch(action) { 323 | this._state = this._reducer(this._state, action) 324 | this._emitChange() 325 | } 326 | 327 | // samotný reducer - musí být čistá funkce 328 | const reducer = (state, { action, payload }) => { 329 | if (action === 'ADD_TODO') { 330 | state.todos.push({ 331 | text: payload.todoText, 332 | id: payload.id 333 | }) 334 | } 335 | 336 | return state 337 | } 338 | 339 | // předání reduceru 340 | new Store(initialState, reducer) 341 | // Tadáá 342 | ``` 343 | A to je všechno! Máme naprosto luxusní nový store, který umí naprosto všechno, co naše aplikace bude pro změnu stavu potřebovat. Fakt! 344 | 345 | Teď si to dáme hezky všechno dohromady, to znamená, že si vytvoříme: 346 | - `StoreContextProvider` 347 | - `Store` 348 | - `Actions creators` 349 | - `connect()` funkce pro pro spojování komonent se storem kdekoliv ve stromu 350 | 351 | Tak si napimplementujeme přidávání Faktur. Mělo by to fungovat tak, že klikneme na tlačítku `Přidat fakturu`, vyskočí modální okno a tam bude jednoduchý formulář pro přidání faktury. 352 | 353 | Do toho! Uvidíme co stihneme, příště to napojíme na Firebasku a live updaty! 354 | -------------------------------------------------------------------------------- /pt.5-React-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pt.5-react-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "avocode-ui": "^7.1.2", 7 | "false": "0.0.4", 8 | "react": "^15.5.4", 9 | "react-dom": "^15.5.4" 10 | }, 11 | "devDependencies": { 12 | "react-scripts": "1.0.7" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pt.5-React-redux/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdev-js-evenings/react-workshop/31a0a48f1a5419003195abbb8dc19f90e88f4a62/pt.5-React-redux/public/favicon.ico -------------------------------------------------------------------------------- /pt.5-React-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
    29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /pt.5-React-redux/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Helvetica, sans-serif; 3 | } 4 | 5 | .App { 6 | text-align: center; 7 | } 8 | 9 | .App-logo { 10 | animation: App-logo-spin infinite 20s linear; 11 | height: 80px; 12 | } 13 | 14 | .App-header { 15 | background-color: #222; 16 | height: 150px; 17 | padding: 20px; 18 | color: white; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | .column { 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .row { 31 | display: flex; 32 | flex-direction: row; 33 | } 34 | 35 | .flex-1 { 36 | flex: 1; 37 | } 38 | 39 | .flex-2 { 40 | flex: 2; 41 | } 42 | 43 | .flex-3 { 44 | flex: 3; 45 | } 46 | 47 | .flex-4 { 48 | flex: 4; 49 | } 50 | 51 | .flex { 52 | flex: 1; 53 | } 54 | 55 | .flexbox { 56 | display: flex; 57 | } 58 | 59 | table { 60 | table-layout: fixed; 61 | width: 100%; 62 | } 63 | 64 | td { 65 | text-align: left; 66 | } 67 | 68 | th { 69 | font-weight: normal; 70 | background: #e6e6e6; 71 | } 72 | 73 | .key { 74 | font-weight: bold; 75 | } 76 | 77 | .tax { 78 | padding: 2em 0; 79 | font-size: 1.5em; 80 | color: #D40455; 81 | } 82 | 83 | .modal-container { 84 | position: fixed; 85 | top: 0; 86 | left: 0; 87 | width: 100%; 88 | min-height: 100%; 89 | } 90 | 91 | .modal { 92 | background: white; 93 | width: 500px; 94 | text-align: left; 95 | top: 50%; 96 | left: 50%; 97 | transform: translate(-50%, -50%); 98 | position: absolute; 99 | overflow-y: scroll; 100 | } 101 | 102 | .centered { 103 | text-align: center; 104 | } 105 | 106 | .modal-content { 107 | padding: 1em 2em; 108 | 109 | } 110 | 111 | .modal-title { 112 | background: blue; 113 | color: white; 114 | font-weight: lighter; 115 | margin: 0; 116 | padding: .3em 0; 117 | font-size: 1.2em; 118 | } 119 | 120 | .modal-overlay { 121 | position: absolute; 122 | top: 0; 123 | left: 0; 124 | width: 100%; 125 | height: 100%; 126 | background: rgba(0, 0, 0, 0.5); 127 | } 128 | 129 | .form-control { 130 | font-size: 1em; 131 | padding: .3em .5em; 132 | } 133 | 134 | .form-group { 135 | margin: 1em 0; 136 | } 137 | 138 | .btn { 139 | background-color:#44c767; 140 | -moz-border-radius:3px; 141 | -webkit-border-radius:3px; 142 | border-radius:3px; 143 | border:1px solid #18ab29; 144 | display:inline-block; 145 | cursor:pointer; 146 | color:#ffffff; 147 | font-family:Arial; 148 | font-size:17px; 149 | padding: 8px 16px; 150 | text-decoration:none; 151 | text-shadow:0px 1px 0px #2f6627; 152 | } 153 | .btn:hover { 154 | background-color:#5cbf2a; 155 | } 156 | .btn:active { 157 | position:relative; 158 | top:1px; 159 | } 160 | 161 | .btn-danger { 162 | background-color:#c23a41; 163 | -moz-border-radius:3px; 164 | -webkit-border-radius:3px; 165 | border-radius:3px; 166 | border:1px solid #850808; 167 | display:inline-block; 168 | cursor:pointer; 169 | color:#ffffff; 170 | font-family:Arial; 171 | font-size:17px; 172 | padding: 8px 16px; 173 | text-decoration:none; 174 | text-shadow:0px 1px 0px #2f6627; 175 | } 176 | .btn-danger:hover { 177 | background-color:#B40455; 178 | } 179 | .btn-danger:active { 180 | position:relative; 181 | top:1px; 182 | } 183 | 184 | .form-buttons { 185 | text-align: center; 186 | padding: 1em 0; 187 | } 188 | 189 | .form-buttons .btn { 190 | margin: 0 .5em; 191 | } 192 | 193 | label { 194 | display: block; 195 | } 196 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | 4 | import InvoiceTable from './components/invoice-table' 5 | import TaxesCalculator from './components/taxes-calculator' 6 | import { connect } from './store' 7 | 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 |
    13 |
    14 |

    Faktury

    15 | 16 |
    17 |
    18 |

    Daně

    19 | 20 |
    21 |
    22 | ) 23 | } 24 | } 25 | 26 | export default connect({}, {})(App); 27 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export const openInvoiceModal = (invoiceId) => { 2 | return { 3 | action: 'INVOICE_MODAL_OPEN', 4 | payload: { 5 | id: invoiceId, 6 | } 7 | } 8 | } 9 | 10 | export const closeInvoiceModal = () => { 11 | return { 12 | action: 'INVOICE_MODAL_CLOSE', 13 | payload: {} 14 | } 15 | } 16 | 17 | export const onInvoicePropertyChange = (invoiceProperty, value) => { 18 | return { 19 | action: 'UPDATE_NEXT_INVOICE', 20 | payload: { 21 | invoiceProperty, value, 22 | } 23 | } 24 | } 25 | 26 | 27 | export const addInvoice = () => { 28 | return { 29 | action: 'ADD_NEXT_INVOICE', 30 | payload: {}, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/components/invoice-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Modal from './modal' 4 | 5 | import { connect } from '../store' 6 | 7 | import * as actions from '../actions' 8 | 9 | 10 | export default connect({}, actions)((props) => { 11 | 12 | const handleSubmit = (e) => { 13 | e.preventDefault() 14 | props.addInvoice() 15 | } 16 | 17 | const onInvoicePropertyChange = (property) => (e) => { 18 | props.onInvoicePropertyChange(property, e.target.value) 19 | } 20 | 21 | if (Object.keys(props.nextInvoice).length === 0) { 22 | return null 23 | } 24 | 25 | return ( 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 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/components/invoice-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import InvoiceForm from './invoice-form' 4 | 5 | import { connect } from '../store' 6 | 7 | import * as actions from '../actions' 8 | 9 | 10 | class InvoiceTable extends React.PureComponent { 11 | state = { 12 | addInvoice: false, 13 | } 14 | 15 | 16 | _handleAddInvoiceToggle = () => { 17 | // this.setState({ addInvoice: !this.state.addInvoice }) 18 | if (Object.keys(this.props.nextInvoice).length === 0) { 19 | this.props.openInvoiceModal(Date.now()) 20 | } else { 21 | this.props.closeInvoiceModal() 22 | } 23 | } 24 | 25 | render() { 26 | return ( 27 |
    28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {this.props.invoices.map(invoice => { 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ) 50 | })} 51 | 52 | 53 |
    #OdběratelČástkaDPHCelkem
    {invoice.id}{invoice.customer}{invoice.price}{invoice.VAT}{invoice.total}
    54 | 55 |
    56 | ) 57 | } 58 | } 59 | 60 | export default connect({}, actions)(InvoiceTable) 61 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/components/modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | export default ({ children, title, onHideRequest }) => { 5 | return ( 6 |
    7 |
    8 |
    9 | {title &&

    {title}

    } 10 |
    {children}
    11 |
    12 |
    13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/components/taxes-calculator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | const TaxesCalculator = ({ income, tax, taxRatio, costsRatio }) => { 5 | 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    Příjem{income.toLocaleString()},-
    Nákladový paušál 15 | 19 |
    Náklady {costsRatio}%35000,-
    Základ daně 70000,-
    Daň {taxRatio}%{tax.toLocaleString()},-
    35 | ) 36 | } 37 | 38 | export default TaxesCalculator 39 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | import createStore, { initialData } from './store' 7 | import reducer from './store/reducer' 8 | 9 | 10 | const store = createStore(initialData, reducer) 11 | 12 | class StoreProvider extends React.PureComponent { 13 | static childContextTypes = { 14 | store: React.PropTypes.object.isRequired, 15 | } 16 | 17 | getChildContext() { 18 | return { 19 | store: this.props.store 20 | } 21 | } 22 | 23 | render() { 24 | return this.props.children 25 | } 26 | } 27 | 28 | 29 | 30 | window.store = store 31 | ReactDOM.render( 32 | , 33 | document.getElementById('root') 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/store/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | export const initialData = { 5 | vatRatio: 21, 6 | invoices: [{ 7 | 'id': '2017/1', 8 | 'customer': 'Avocode Inc.', 9 | 'price': 123000, 10 | 'VAT': 25830, 11 | 'total': 148830, 12 | }], 13 | tax: { 14 | income: 123000, 15 | costsRatio: 60, 16 | taxRatio: 15, 17 | tax: 7380, 18 | }, 19 | nextInvoice: { 20 | }, 21 | user: { 22 | username: '', 23 | id: 0, 24 | email: '', 25 | } 26 | } 27 | 28 | 29 | export const connect = (stateToProps = {}, actions = {}) => (Component) => { 30 | return class extends React.Component { 31 | static contextTypes = { 32 | store: React.PropTypes.object.isRequired, 33 | } 34 | 35 | state = this.context.store.getState() 36 | 37 | componentDidMount() { 38 | this.context.store.listen(this._handleStoreChange) 39 | } 40 | 41 | componentWillUnmout() { 42 | this.context.store.unlisten(this._handleStoreChange) 43 | } 44 | 45 | _handleStoreChange = () => { 46 | const stateKeys = Object.keys(stateToProps) 47 | const state = stateKeys.reduce((state, stateKey) => { 48 | return { 49 | [stateToProps[stateKey]]: state[stateKey], // transform to keys from stateProps = {'stateKey': 'propsKey'} 50 | } 51 | }, this.context.store.getState(Object.keys(stateKeys))) 52 | 53 | this.setState(state) 54 | } 55 | 56 | _prepareActions() { 57 | const actionKeys = Object.keys(actions) 58 | return actionKeys.reduce((wrappedActions, actionKey) => { 59 | wrappedActions[actionKey] = (...args) => { 60 | this.context.store.dispatch(actions[actionKey](...args)) 61 | } 62 | 63 | return wrappedActions 64 | }, {}) 65 | } 66 | 67 | render() { 68 | const props = { 69 | ...this.state, 70 | ...this.props, 71 | ...this._prepareActions(), 72 | } 73 | 74 | return 75 | } 76 | } 77 | } 78 | 79 | class Store { 80 | _listeners = [] 81 | _state = {} 82 | _reducer = null 83 | 84 | constructor(initialState, reducer) { 85 | this._state = initialState 86 | this._reducer = reducer 87 | } 88 | 89 | _emitChange() { 90 | this._listeners.forEach(listener => listener()) 91 | } 92 | 93 | listen(listener) { 94 | this._listeners.push(listener) 95 | } 96 | 97 | unlisten(listener) { 98 | this._listeners = this._listeners.filter(candidate => candidate !== listener) 99 | } 100 | 101 | getState(keys = []) { 102 | return keys.reduce((state, key) => { 103 | return { 104 | [key]: state[key], 105 | } 106 | }, this._state) 107 | } 108 | 109 | dispatch(action) { 110 | this._state = this._reducer(this._state, action) 111 | console.info('Action dispatched:', action.action, action.payload, this._state) 112 | this._emitChange() 113 | } 114 | } 115 | 116 | 117 | export default (initialState, reducer) => new Store(initialState, reducer) 118 | -------------------------------------------------------------------------------- /pt.5-React-redux/src/store/reducer.js: -------------------------------------------------------------------------------- 1 | export default (state, { action, payload }) => { 2 | switch (action) { 3 | case 'INVOICE_MODAL_OPEN': 4 | return { 5 | ...state, 6 | nextInvoice: { 7 | id: payload['id'], 8 | } 9 | } 10 | 11 | case 'INVOICE_MODAL_CLOSE': 12 | return { 13 | ...state, 14 | nextInvoice: {} 15 | } 16 | 17 | case 'UPDATE_NEXT_INVOICE': 18 | return { 19 | ...state, 20 | nextInvoice: { 21 | ...(state.nextInvoice || {}), 22 | [payload['invoiceProperty']]: payload['value'], 23 | } 24 | } 25 | 26 | 27 | case 'ADD_NEXT_INVOICE': 28 | return { 29 | ...state, 30 | nextInvoice: {}, 31 | invoices: state.invoices.concat([state.nextInvoice]) 32 | } 33 | } 34 | 35 | return state 36 | } 37 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "semi": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/README.md: -------------------------------------------------------------------------------- 1 | ![Nautč se React.js](https://image.ibb.co/dnb7tv/react_event_fb_title_v03.png) 2 | 3 | # Naučte se React.js 7 - advanced Redux 4 | ## Minule 5 | Jsme si tedy ukázali tu nejzákladnější architekturu Reduxu. Řekli jsme si, že je to jenom implementace fluxu a celá jeho existence je opravdu jednoduchoučká. 6 | 7 | Skočili jsme ve chvíli, kdy šlo již přidávat faktury do naší skvělé aplikace. 8 | 9 | ## Dneska 10 | Dneska tedy přijde na řadu pokročilá práce s Reduxem. To znamená že si naimplmenentíme: 11 | - routování 12 | - lenses ("koukání" do stavu přes čočky) 13 | - více než jeden reducer 14 | - middleware 15 | - asynchronní volání 16 | 17 | Ale nejdřív si trošku celý ten Store zrefaktorujeme, protože je to příliš mnoho kódu. Redux se dá napsat na tři řádky a tak to taky uděláme! 18 | 19 | A pak se hned vrhneme na to routování. Bylo by totiž fajn, kdyby se modal pro přidání / úpravu faktury zobrazoval jakmile přijdeme a nějakou URL, to by bylo mega cool. 20 | 21 | Tak se na to vrhneme. 22 | 23 | ## Lehký refaktor storu 24 | Store je vlastně primitivní komponenta, která má naprosto jednoduché API, vlastně jde jenom o tři metody: 25 | - `getState()` 26 | - `dispatch()` 27 | - `listen()` 28 | 29 | Pokud potřebujeme jen tři metody, tak fakt není potřeba vytvářet třída, která navíc má všechny své properties public, takže si to zabalíme všechno do closure: 30 | ```js 31 | 32 | const createStore = (initialState, initialReducers) => { 33 | let listeners = [] 34 | let state = initialState 35 | let reducers = initialReducers 36 | 37 | const _emitChange = () => { 38 | listeners.forEach(listener => listener()) 39 | } 40 | 41 | const listen = (listener) => { 42 | listeners.push(listener) 43 | } 44 | 45 | const unlisten = (listener) => { 46 | listeners = listeners.filter(candidate => candidate !== listener) 47 | } 48 | 49 | const getState = (keys = []) => { 50 | return keys.reduce((state, key) => { 51 | return { 52 | [key]: state[key], 53 | } 54 | }, state) 55 | } 56 | 57 | const dispatch = (action) => { 58 | state = reducers.reduce((reducedState, reducer) => { return reducer(reducedState, action) }, state) 59 | console.info('Action dispatched:', action.action, action.payload, state) 60 | _emitChange() 61 | } 62 | 63 | return { 64 | listen, 65 | unlisten, 66 | dispatch, 67 | getState, 68 | } 69 | } 70 | ``` 71 | 72 | Nic moc změna, pořád se vlastně jedná o třídu. Jediné relevantní vylepšení je to, že se nedá ke stavu přistupovat jinak než přes `getState()` to samé platí o ostatních proměnných ve funkci `createStore()`. 73 | 74 | Zjednodušování by mohlo jít mnohem dál, kdyby React byl snadno schopný přijímat nový context. To zatím možné není, ale prozradím vám, že dostačující implmenetace Reduxu vypadá takto: 75 | ```js 76 | const createDispatch = (initialState, reducers, onChange) => { 77 | return (action) => { 78 | const nextState = reducers.reduce((reducedState, reducer) => { 79 | return reducer(reducedState, action) 80 | }, initialState) 81 | 82 | onChange(nextState, createDispatch(nextState, reducers, onChange)) 83 | } 84 | } 85 | 86 | const renderApp = (state, dispatch) => { 87 | console.log(state) 88 | window.dispatch = dispatch 89 | return 90 | } 91 | 92 | const dispatch = createDispatch({ 'actionCounter': 0 }, [(state) => ({ 93 | ...state, 94 | actionCounter: state.actionCounter + 1, 95 | })], renderApp) 96 | 97 | window.dispatch = dispatch 98 | // zavolání dispatch() pak změní stav aplikace 99 | ``` 100 | Funkční ukázku si můžete prohlédnou [zde](./store/mini-redux.js). 101 | 102 | No to jentak na okraj. Zatím to necháme roztahanější a trochu čitelnější, abychom se v toho nezbláznili. 103 | 104 | 105 | ## První primitivní middleware 106 | Middleware? Co je to middleware. V jazyce Reduxu je middleware funkce obalující klíčovou funkci `dispatch()`. 107 | 108 | Middleware musí splňovat tu podmínku, že vrací vždy novou funkci, to je celé, nic víc není potřeba. Aby takový argument měl smysl, musí jako argument dostávat původní dispatch funkci. 109 | 110 | A k čemu je takový middleware praktický? Tak například - k logování. 111 | Představte si, že chceme logo jaká akce jde do dispatch funkce a jaký je nový stav po aplikace téhle funkce. Takový middleware by vypadal tedy takto: 112 | ```js 113 | const loggerMiddleware = (dev = true) => (originalDispatch) => { 114 | const logIt = (title, toLog) => { 115 | if (!dev) { 116 | return 117 | } 118 | 119 | console.log(title, toLog) 120 | } 121 | 122 | return (action) => { 123 | const nextState = originalDispatch(action) 124 | logIt('Action:', action) 125 | logIt('State:', nextState) 126 | return nextState 127 | } 128 | } 129 | ``` 130 | 131 | Je to jasné? Mimochodem, funkci `logIt()` bychom si mohli prostě předat jako argument, docela elegantní, ne? 132 | Jak takový middleware ale teď použít? Mnooo, tak zřejmě ho musíme předat do storu při jeho konfiguraci: 133 | ```js 134 | const store = createStore(initialData, [reducer], [loggerMiddleware]) 135 | ``` 136 | Takže je potřeba upravit `createStore()` funkci. A to tak, aby přijímala ještě pole middlewareů, které bude potřeba aplikovat a ještě naimplementovat funkci uvnitř storu, která se bude jmenovat `creatDispatchWithMiddleware()` a bude apliakovat middlewary. 137 | 138 | Jak taková aplikace middleware bude vypadat? No. Máme pole funkcí, které vrací obalené dispatche a potřebueje z toho udělat jednu funkci... Tak to je prostě reducke, ne? 139 | ```js 140 | const createDispatchWithMiddleware = (middleWares) => { 141 | return middleWares.reduce((originalDispatch, middleware) => { 142 | return middleware(originalDispatch) 143 | }, dispatch) 144 | } 145 | ``` 146 | To je docela elegantní, ne? 147 | 148 | A funguje to? No však si tenhle middleware zkuste aplikovat :). 149 | 150 | Oukej, takže máme middleware pro logování. 151 | 152 | Za chvíli se vrhenem na komplikovanější middleware! 153 | 154 | ## Async 155 | Tak nejdřív, abychom mohli bejt async, tak si musíme popovídat o Dependency Injection. 156 | 157 | ### Dependency Injection 158 | Tohle je už takové zaklínadlo, že. Psali o tom daleko zkušenější a lepší progáči, než jsem já, proto si doporučuju přečíst třeba [tento článek od Davida Grudla](https://phpfashion.com/co-je-dependency-injection). 159 | 160 | Dočteno, super. Prostě jde o to, abychom předávali závislosti přímo tam, kde jsou potřeba a v našem případě to neděláme kvůli "čistotě" kódu, ale **hlavně** kvůli testovatelnosti. 161 | 162 | Vezměme si například tuhle closurku (closure proto, protože funkce používá proměnné z vyšší scope - `fetch`): 163 | ```js 164 | export default const fetchInvoice = (invoiceId) => { 165 | return fetch('invoices/' + invoiceId) 166 | } 167 | ``` 168 | Moje otázka zní - jak byste testovali takový kód. 169 | 170 | Každý asi umí otestovat tuhle funkci: 171 | ```js 172 | const addNumbers = (a, b) => { 173 | return a + b 174 | } 175 | 176 | // test 177 | const five = addNumbers(2, 3) 178 | 179 | if (five !== 5) { 180 | throw new Error('Test failed!!') 181 | } 182 | ``` 183 | Rozumíme? OK. Tohlae je přece jednoduché. Neboť `addNumbers()` jest čistá funkce, která pouze a jenom sčítá dvě čísla. 184 | 185 | Ovšem ale v příkladu s funkci `fetchInvoie()` jsem docela v háji, ne? Jak dokážeme "dostat" do chvíle, kde `fetch()` začne něco dělat v síti, jak dokážeme tohle nějak namockovat? Abychom otestovali, že funkce dělá, to co má? 186 | 187 | To nejde... S takovouhle funkcí to prostě **nejde**. A proč to nejde? No proto, že využívá globální proměnné! 188 | 189 | Vždyť `fetch()` je globální proměnná nebo snad ne? A co nás učili na základní škole? Globální proměnné jsou zlo! 190 | 191 | Takže funkci upravíme: 192 | ```js 193 | export default const fetchInvoice = (invoiceId, customFetch) => { 194 | return customFetch('invoices/' + invoiceId) 195 | } 196 | ``` 197 | Výborně, custom api si už hezky předáváme a teď jak to otestujeme? 198 | ```js 199 | const mockResponse = { 200 | 'id': 123, 201 | 'customer': 'Avocode', 202 | ... 203 | } 204 | const customFetchMock = () => { 205 | return Promise.resolve(mockResponse) 206 | } 207 | 208 | fetchInvoice(123, customFetchMock).then((result) => { 209 | if (result !== mockResponse) { 210 | throw new Error('Test failed!') 211 | } 212 | }) 213 | ``` 214 | Tadá! Testovatelný kód FTW! Stačí pouze psát takový kód, který **vrací** výsledky a všechny proměnné, které potřebuje pro svůj chod vždy dostává přes parametry (klidně i v closure). 215 | 216 | ### DI Container 217 | Samozřejmě je otravný všechny pořád někam předávat a tak by asi nebylo od věci, kdybychom si nějak chytře všechny služby předávali. 218 | 219 | V našem příkladu budeme používat `firebase`, což je externí služby tj. typický kandidát pro předávání jako závislost. 220 | 221 | Samozřejmě to můžeme naprasit a ze souboru si vyexportovat singleton a ten všude importovat: 222 | ```js 223 | const config = { 224 | apiKey: "AIzaSyA5jKFg6VsSLkfKeeTz1ZrFfI3z6NML__0", 225 | authDomain: "state-container.firebaseapp.com", 226 | databaseURL: "https://state-container.firebaseio.com", 227 | projectId: "state-container", 228 | storageBucket: "state-container.appspot.com", 229 | messagingSenderId: "728941706922" 230 | }; 231 | 232 | const client = firebase.initializeApp(config) 233 | client.auth().signInAnonymously() 234 | 235 | export default client.database() 236 | 237 | 238 | // někde jinde 239 | import database from './firebase' 240 | ``` 241 | Ale jak víme, to je prostě prasárna největšího kalibru, že. Proto je prostě lepší vymylset si mechanismus, který bude do akcí předávat závislosti. 242 | 243 | Samozřejmě i tohle **je** prasárna, ale zatím jsem nenašel lepší a pohodlenější způsob jak s DI pracovat. Ano, dá se na to použít monády, ale tam ještě zdaleka nejsem / jsme :). 244 | 245 | Tak jak si to představuji. 246 | 247 | ## DI v Reduxu 248 | Víme, že action creatory musí projít přes `connect()`, kde se propojují s funkcí `dispatch()` jinak jsou prd platné, že? Tudíž by bylo super, kdybychom nějak v action creatoru řekli storu, aby nám poslal služby, které při konfiguraci získá: 249 | ```js 250 | // poskytneme storu data přes unikátní klíče 251 | const store = createStore(initialData, [reducer], [], { 252 | firebase: client, 253 | database: client.database() 254 | }) 255 | 256 | // v actions.js 257 | const requestInvoiceFromApi = (invoiceId, { database }) => { 258 | return database.ref(`users/${id}`).once('value') 259 | } 260 | requestInvoiceFromApi.services = [ 261 | // takhle si vyžádám service database do objektu který bude vždy poslední parametr funkce 262 | 'database' 263 | ] 264 | ``` 265 | Tak jdeme na to, ne? Stačí jenom servicy uložit do storu, přidat k nim getter a pak ve funkci connect je vytáhnout do akcí. 266 | 267 | Takže bude potřeba upravit funkce `connect()` která spojuje akce se Reactem a `dispatch()` funkcí: 268 | ```js 269 | _createAction = (action) => { 270 | const services = this.context.store.getServices() 271 | const requestedServices = (action.services || []).reduce((providedServices, serviceName) => { 272 | return { 273 | ...providedServices, 274 | [serviceName]: services[serviceName], 275 | } 276 | }, {}) 277 | 278 | return (...args) => { 279 | this.context.store.dispatch(action(...(args.concat([requestedServices])))) 280 | } 281 | } 282 | 283 | _prepareActions() { 284 | const actionKeys = Object.keys(actions) 285 | return actionKeys.reduce((wrappedActions, actionKey) => { 286 | wrappedActions[actionKey] = this._createAction(actions[actionKey]) 287 | 288 | return wrappedActions 289 | }, {}) 290 | } 291 | ``` 292 | Takže jsme si vytvořili metodu `_createAction()`, která zase jenom obalí původní action creator (funkce vracející akci) a podle klíčů, které může action creator mít v property `services` (funkce v Javascriptu je také objekt), ji předáme jako poslední argument požadovaný objekty se závislostmi. 293 | 294 | No není to ale elegantní? Samozřejmě by to šlo vyřešit i jinou cestou, ale tohle je myslím dostatečné, zatím... 295 | 296 | ## API 297 | Takže máme všechno připraveno pro to, abychom si aplikačku připojili k nějakému APIčku. 298 | 299 | Já jsem už připravil Firebasku, kam budeme ukládat fakturky a začneme zlehka. Nejdřív si upravíme seznam faktur tak, aby přijímal faktury z api a při jejich načítání zobrazil nějakou načítácí komponentu. Na to se samozřejmě bude hodit nějaká chytrá komponentička, která obstará request. Sic je pravda, že by fetchování dat **nemělo** být v komponentách, ale zatím si to zjednodušíme a prostě pokud se daná komponenta namountuje, tak udělá request, jakmile se request vykoná, tak vykreslí své děti, takovéto api: 300 | ```js 301 | const renderAsync = ({ url }) => { 302 | return ( 303 | } errorComponent={}> 304 | {(apiInvoices) => ( 305 | 306 | )} 307 | 308 | ) 309 | } 310 | ``` 311 | To by bylo docela cool, ne? Jdeme na to! My tedy budeme používat firebase, proto si komponentu můžeme uzpůsobit tak, aby dělala vždy requesty do firebasky. 312 | 313 | Takže jsem takovou komponentičku urobil [tuhle](./src/components/lazy-load.js). Mrkněte se na ni, je fakt jednoduchoučká. 314 | 315 | Zajímavější ale bude spíš sledovat, jak se budou aktualizovat fakturky, že? Tak umíme jednoduše fakturu přidat, teď by to chtělo ji ale ještě poslat do APIčka. 316 | 317 | Na tohle samozřejmě slouží pouze a jenom action creatory - známe přeci schéma fluxu. Takže prostě k přidání faktury ještě přidáme volání do API, která fakturu přidá. 318 | 319 | Skončíme s něčím takovým: 320 | ```js 321 | export const addInvoice = inject(['database', 'getState'])(async (formInvoiceProps, { database, getState }) => { 322 | const nextInvoice = getState().nextInvoice 323 | if (Object.keys(nextInvoice) === 0) { 324 | return 325 | } 326 | 327 | const invoice = { 328 | ...nextInvoice, 329 | ...formInvoiceProps, 330 | } 331 | 332 | 333 | await database.ref(`/invoices/${nextInvoice.id}`).set(invoice) 334 | 335 | return { 336 | action: 'ADD_NEXT_INVOICE', 337 | payload: { invoice }, 338 | } 339 | }) 340 | ``` 341 | 342 | Huh, komplikované? To jo. Ještě ke všemu budeme potřebovat nějaký hezký middleware. Protože takový action creator už dávno nevrací pouhou akci. Jedná se o asynchronní funkci, která vrací vždy promise a až jako argument metody `then()` přichází výsledek action creator. 343 | 344 | Zajímavé ale je to, že do API posíláme to samé, co si ukládáme lokálně, můžeme tedy klidně `await a async` smazat a v klidu si nechat teď už nečistou funkci, která ovšem nebude čekat na výsledek zapisání do firebasky, tedy všechno se zapíše okamžitě. 345 | 346 | No, zatím to tak ale necháme. 347 | 348 | Je to jiný problém. 349 | 350 | ## Routing není jen tak... 351 | Máme komponentu InvoiceTable, která má sobě načítání pole faktur, které získává přes LazyLoad komponentu. To je supr, pokud se tahle komponenta má vykreslit jednou při startu aplikace, ale jak pak donutit LazyLoad aby refreshnul seznam faktur, pokud jsme ho změnili? Jasný, je to firebase, takže by šlo prostě poslouchat na změny, ale to my nechceme. 352 | 353 | Chceme mít přece pravdu uloženou ve stavu a nikde jinde. To, že v APIčku jsou data uložená jinak nás moc netrápí. Nějaká API odpověď by pro nás přece měla být akce, ne? Stav aplikace se dá pouze měnit akcemi. 354 | 355 | Takže jak teda tenhle problém vyřešit? Je samozřejmě jasné, určitě chceme refreshnout data jakmile si zobrazíme nějakou url, to je jasný. Tj. chceme refreshnout seznam faktur pokud: 356 | - přijdeme na url 357 | - změní se url 358 | - lokálně změníme seznam faktur 359 | - asi trilion dalších... 360 | 361 | Huh, co s tím. No, bohužel žijeme tak trochu v době kamenné, neboť prostě, když víme, že měníme stav nějakého seznamu, tak musíme otrocky zavolat nějakou refresh funkci, která faktury refreshne. Prostě se s tím nedá nic dělat :(... 362 | 363 | Ovšem, situace je docela fajn, pokud máme to štěstí a používáme firebase. Ta totiž funguje přes websockety a tak je jednoduché prostě jenom poslouchat na změny ve firebase a dispatchovat tuhle akci. 364 | 365 | Ke změně stavu potřebujeme pouze referenci na funkci `dispatch()` nic víc není potřeba. Takže pokud máme funkci `dispatch()` nic nám nebrání v: 366 | ```js 367 | database.ref('/invoices').on('value', snapshot => dispatch({ 368 | type: 'REFRESH_INVOICE', 369 | payload: { invoices: toArray(snapshot) }, 370 | }) 371 | ``` 372 | A je to... Tohle zajistí kontinuální aktualizaci faktur, aniž bychom je museli při každé změně aktualizovat ručně. 373 | 374 | Tohle je ovšem hudba budoucnosti a tak budeme muset prostě pořád volat funkci `refreshInvoices()` :(... 375 | 376 | ### Routing 377 | A jak je to se změnou routy? No tak jak jsme si říkali. Jediná věc, která může změnit stav aplikace je akce. Takže změna URL je prostě a jenom změny routy, nemyslíte? 378 | 379 | Máme-li `dispatch()` můžeme změnit i routy: 380 | ```js 381 | window.addEventListener('popstate', () => { 382 | dispatch({ 383 | type: 'CHANGE_ROUTE', 384 | payload: { route: window.location.href + window.location.search }, 385 | }) 386 | }) 387 | ``` 388 | Tadá! 389 | 390 | Přidat refreshování přes APIčko pak není problém, ne? Vyzkoušíme! 391 | 392 | No a když máme tohle, tak co matchování rout? To je přece také snadňoučké! 393 | ```js 394 | const routes = { 395 | '/ahoj': (uri) => { 396 | console.log('ahoj uri', uri) 397 | } 398 | } 399 | 400 | const match = (routes, uri) => { 401 | const parsedUri = parse(uri, true) 402 | const { pathname } = parsedUri 403 | if (routes[pathname]) { 404 | return routes[pathname](parsedUri) 405 | } 406 | 407 | return null 408 | } 409 | ``` 410 | Funkce `match()` bere objekt route a ty pak matchuje s url, která je jí poslaná. Pokud route odpovídá, tak se nastaví. 411 | 412 | Takto je primitivní udělat například stránku 404 nebo případně si zavolat action creator, který by třeba refreshnul invoicy. Máte pocit, že je to složité? Já ne. 413 | 414 | Routu je samozřejmě žádoucí předávat do stavu a pak si můžete udělat komponetu ``, která by prostě zobrazovala svoje děti v závislosti na tom, jestli klíč `route` ve stavu odpovídá tomu, které jste jí předali v property `path`. No není to krásné? To si taky napíšeme! 415 | 416 | Nejhezčí příklad by bylo, kdybychom pomocí routy dokázali zobrazit modal k úpravě invoicy. Modal je jednoduchá komponenta, která prostě a jenom bere jako argument invoice, pokud je nastavená, nebude tedy problém ho upravit, aby si mohl tahat ID invoicy z URL a pak se prostě zobrazil, to by byla krása... 417 | 418 | Trošku samozřejmě bude problém to, že se bude muset na faktury "pokčat", ale to se dá nastavit jedním flagem ve storu, ne? Paráda! 419 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pt.5-react-redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "avocode-ui": "^7.1.2", 7 | "false": "0.0.4", 8 | "react": "^15.5.4", 9 | "react-dom": "^15.5.4", 10 | "uuid": "^3.0.1" 11 | }, 12 | "devDependencies": { 13 | "react-scripts": "1.0.7" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdev-js-evenings/react-workshop/31a0a48f1a5419003195abbb8dc19f90e88f4a62/pt.6-React-advanced-redux/public/favicon.ico -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
    29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Helvetica, sans-serif; 3 | } 4 | 5 | .App { 6 | text-align: center; 7 | } 8 | 9 | .App-logo { 10 | animation: App-logo-spin infinite 20s linear; 11 | height: 80px; 12 | } 13 | 14 | .cell-right { 15 | text-align: right; 16 | padding-right: 5px; 17 | } 18 | 19 | .total { 20 | font-weight: bold; 21 | font-size: 1.1em; 22 | } 23 | 24 | .App-header { 25 | background-color: #222; 26 | height: 150px; 27 | padding: 20px; 28 | color: white; 29 | } 30 | 31 | .App-intro { 32 | font-size: large; 33 | } 34 | 35 | .column { 36 | display: flex; 37 | flex-direction: column; 38 | } 39 | 40 | .row { 41 | display: flex; 42 | flex-direction: row; 43 | } 44 | 45 | .flex-1 { 46 | flex: 1; 47 | } 48 | 49 | .flex-2 { 50 | flex: 2; 51 | } 52 | 53 | .flex-3 { 54 | flex: 3; 55 | } 56 | 57 | .flex-4 { 58 | flex: 4; 59 | } 60 | 61 | .flex { 62 | flex: 1; 63 | } 64 | 65 | .flexbox { 66 | display: flex; 67 | } 68 | 69 | table { 70 | table-layout: fixed; 71 | width: 100%; 72 | } 73 | 74 | td { 75 | text-align: left; 76 | } 77 | 78 | th { 79 | font-weight: normal; 80 | background: #e6e6e6; 81 | } 82 | 83 | .key { 84 | font-weight: bold; 85 | } 86 | 87 | .tax { 88 | padding: 2em 0; 89 | font-size: 1.5em; 90 | color: #D40455; 91 | } 92 | 93 | .modal-container { 94 | position: fixed; 95 | top: 0; 96 | left: 0; 97 | width: 100%; 98 | min-height: 100%; 99 | } 100 | 101 | .modal { 102 | background: white; 103 | width: 500px; 104 | text-align: left; 105 | top: 50%; 106 | left: 50%; 107 | transform: translate(-50%, -50%); 108 | position: absolute; 109 | overflow-y: scroll; 110 | } 111 | 112 | .centered { 113 | text-align: center; 114 | } 115 | 116 | .modal-content { 117 | padding: 1em 2em; 118 | 119 | } 120 | 121 | .modal-title { 122 | background: blue; 123 | color: white; 124 | font-weight: lighter; 125 | margin: 0; 126 | padding: .3em 0; 127 | font-size: 1.2em; 128 | } 129 | 130 | .modal-overlay { 131 | position: absolute; 132 | top: 0; 133 | left: 0; 134 | width: 100%; 135 | height: 100%; 136 | background: rgba(0, 0, 0, 0.5); 137 | } 138 | 139 | .form-control { 140 | font-size: 1em; 141 | padding: .3em .5em; 142 | } 143 | 144 | .form-group { 145 | margin: 1em 0; 146 | } 147 | 148 | .btn { 149 | background-color:#44c767; 150 | -moz-border-radius:3px; 151 | -webkit-border-radius:3px; 152 | border-radius:3px; 153 | border:1px solid #18ab29; 154 | display:inline-block; 155 | cursor:pointer; 156 | color:#ffffff; 157 | font-family:Arial; 158 | font-size:17px; 159 | padding: 8px 16px; 160 | text-decoration:none; 161 | text-shadow:0px 1px 0px #2f6627; 162 | } 163 | .btn:hover { 164 | background-color:#5cbf2a; 165 | } 166 | .btn:active { 167 | position:relative; 168 | top:1px; 169 | } 170 | 171 | .btn-danger { 172 | background-color:#c23a41; 173 | -moz-border-radius:3px; 174 | -webkit-border-radius:3px; 175 | border-radius:3px; 176 | border:1px solid #850808; 177 | display:inline-block; 178 | cursor:pointer; 179 | color:#ffffff; 180 | font-family:Arial; 181 | font-size:17px; 182 | padding: 8px 16px; 183 | text-decoration:none; 184 | text-shadow:0px 1px 0px #2f6627; 185 | } 186 | .btn-danger:hover { 187 | background-color:#B40455; 188 | } 189 | .btn-danger:active { 190 | position:relative; 191 | top:1px; 192 | } 193 | 194 | .form-buttons { 195 | text-align: center; 196 | padding: 1em 0; 197 | } 198 | 199 | .form-buttons .btn { 200 | margin: 0 .5em; 201 | } 202 | 203 | label { 204 | display: block; 205 | } 206 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | 4 | import InvoiceTable from './components/invoice-table' 5 | import TaxesCalculator from './components/taxes-calculator' 6 | import { connect } from './store' 7 | 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 |
    13 |
    14 |

    Faktury

    15 | 16 |
    17 |
    18 |

    Daně

    19 | 20 |
    21 |
    22 | ) 23 | } 24 | } 25 | 26 | export default connect({}, {})(App); 27 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { snapShotToArray } from '../utils' 2 | 3 | 4 | const inject = (services) => (fn) => { 5 | fn.services = services 6 | return fn 7 | } 8 | 9 | export const openInvoiceModal = (invoiceId, { database }) => { 10 | return { 11 | action: 'INVOICE_MODAL_OPEN', 12 | payload: { 13 | id: invoiceId, 14 | } 15 | } 16 | } 17 | 18 | 19 | export const closeInvoiceModal = () => { 20 | return { 21 | action: 'INVOICE_MODAL_CLOSE', 22 | payload: {} 23 | } 24 | } 25 | 26 | export const onInvoicePropertyChange = (invoiceProperty, value) => { 27 | return { 28 | action: 'UPDATE_NEXT_INVOICE', 29 | payload: { 30 | invoiceProperty, value, 31 | } 32 | } 33 | } 34 | 35 | export const refreshInvoices = inject(['database', 'dispatch'])(({ database, dispatch }) => { 36 | database.ref('/invoices/').once().then(result => { 37 | dispatch({ 38 | action: 'INVOICES_REFRESH', 39 | payload: { invoices: snapShotToArray(result) } 40 | }) 41 | }) 42 | }) 43 | 44 | export const addInvoice = inject(['database', 'getState'])(async (formInvoiceProps, { database, getState }) => { 45 | const nextInvoice = getState().nextInvoice 46 | if (Object.keys(nextInvoice) === 0) { 47 | return 48 | } 49 | 50 | const invoice = { 51 | ...nextInvoice, 52 | ...formInvoiceProps, 53 | } 54 | 55 | 56 | await database.ref(`/invoices/${nextInvoice.id}`).set(invoice) 57 | 58 | return { 59 | action: 'ADD_NEXT_INVOICE', 60 | payload: { invoice }, 61 | } 62 | }) 63 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/components/invoice-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Modal from './modal' 4 | 5 | import { connect } from '../store' 6 | 7 | import * as actions from '../actions' 8 | 9 | 10 | export default connect({ 11 | 'nextInvoice': 'invoice', 12 | 'vatRatio': 'VAT', 13 | }, actions)((props) => { 14 | 15 | 16 | const price = Number(props.invoice.price) || 0 17 | const vat = Math.round(price / 100 * Number(props.VAT)) 18 | const total = price + vat 19 | 20 | const handleSubmit = (e) => { 21 | e.preventDefault() 22 | props.addInvoice({ price, vat, total }) 23 | } 24 | 25 | const handleHideRequest = (e) => { 26 | e.preventDefault() 27 | props.onHideRequest() 28 | } 29 | 30 | const onInvoicePropertyChange = (property) => (e) => { 31 | props.onInvoicePropertyChange(property, e.target.value) 32 | } 33 | 34 | if (Object.keys(props.invoice).length === 0) { 35 | return null 36 | } 37 | return ( 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 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/components/invoice-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import InvoiceForm from './invoice-form' 4 | import LazyLoad from './lazy-load' 5 | 6 | import { connect } from '../store' 7 | 8 | import * as actions from '../actions' 9 | 10 | 11 | class InvoiceTable extends React.PureComponent { 12 | state = { 13 | addInvoice: false, 14 | } 15 | 16 | 17 | _handleAddInvoiceToggle = () => { 18 | // this.setState({ addInvoice: !this.state.addInvoice }) 19 | if (Object.keys(this.props.nextInvoice).length === 0) { 20 | this.props.openInvoiceModal(Date.now()) 21 | } else { 22 | this.props.closeInvoiceModal() 23 | } 24 | } 25 | 26 | render() { 27 | return ( 28 |
    29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {(invoices) => ( 42 | 43 | {invoices.map(invoice => { 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ) 53 | })} 54 | 55 | )} 56 | 57 |
    #OdběratelČástkaDPHCelkem
    {invoice.id}{invoice.customer}{invoice.price},-{invoice.vat},-{Number(invoice.total || 0).toLocaleString()},-
    58 | 59 |
    60 | ) 61 | } 62 | } 63 | 64 | export default connect({}, actions)(InvoiceTable) 65 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/components/lazy-load.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { snapShotToArray } from '../utils' 4 | 5 | 6 | 7 | export default class LazyLoad extends React.PureComponent { 8 | static contextTypes = { 9 | database: React.PropTypes.object.isRequired, 10 | } 11 | 12 | state = { 13 | error: null, 14 | loading: false, 15 | result: null, 16 | } 17 | 18 | componentDidMount() { 19 | this.setState({ loading: true }) 20 | this.context.database.ref(this.props.table).once('value').then((result) => { 21 | this.setState({ result: snapShotToArray(result), loading: false }) 22 | }) 23 | .catch((error) => { 24 | console.error('LazyLoad error ->', error) 25 | this.setState({ error }) 26 | }) 27 | } 28 | 29 | render() { 30 | if (this.state.loading) { 31 | return this.props.loadingComponent || null 32 | } 33 | 34 | if (this.state.error) { 35 | return this.props.errorComponent || null 36 | } 37 | 38 | if (this.state.result) { 39 | return this.props.children(this.state.result) 40 | } 41 | 42 | return null 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/components/modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | export default ({ children, title, onHideRequest }) => { 5 | return ( 6 |
    7 |
    8 |
    9 | {title &&

    {title}

    } 10 |
    {children}
    11 |
    12 |
    13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/components/taxes-calculator.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | const TaxesCalculator = ({ income, tax, taxRatio, costsRatio }) => { 5 | 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    Příjem{income.toLocaleString()},-
    Nákladový paušál 15 | 19 |
    Náklady {costsRatio}%35000,-
    Základ daně 70000,-
    Daň {taxRatio}%{tax.toLocaleString()},-
    35 | ) 36 | } 37 | 38 | export default TaxesCalculator 39 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | import StoreProvider from './store/store-provider' 6 | import { parse } from 'url' 7 | 8 | import createStore, { applyMiddleWare, initialData } from './store' 9 | import loggerMiddleware from './store/logger-middleware' 10 | import reducer from './store/reducer' 11 | import firebase from "firebase" 12 | 13 | //import './store/mini-redux' 14 | 15 | 16 | const config = { 17 | apiKey: "AIzaSyA5jKFg6VsSLkfKeeTz1ZrFfI3z6NML__0", 18 | authDomain: "state-container.firebaseapp.com", 19 | databaseURL: "https://state-container.firebaseio.com", 20 | projectId: "state-container", 21 | storageBucket: "state-container.appspot.com", 22 | messagingSenderId: "728941706922" 23 | }; 24 | 25 | const promiseMiddleware = store => dispatch => action => { 26 | if (action instanceof Promise) { 27 | action.then(dispatch) 28 | } else { 29 | dispatch(action) 30 | } 31 | } 32 | 33 | const client = firebase.initializeApp(config) 34 | client.auth().signInAnonymously() 35 | 36 | const database = client.database() 37 | 38 | const store = applyMiddleWare([promiseMiddleware])(createStore)(initialData, [reducer], { 39 | firebase: client, 40 | database, 41 | }) 42 | 43 | const routes = { 44 | '/ahoj': (uri) => { 45 | console.log('ahoj uri', uri) 46 | } 47 | } 48 | 49 | const match = (routes, uri) => { 50 | const parsedUri = parse(uri, true) 51 | const { pathname } = parsedUri 52 | if (routes[pathname]) { 53 | return routes[pathname](parsedUri) 54 | } 55 | 56 | return null 57 | } 58 | 59 | window.addEventListener('popstate', () => { 60 | const action = match(routes, window.location.href + window.location.search) 61 | 62 | if (!action) { 63 | return 64 | } 65 | 66 | store.dispatch(action) 67 | }) 68 | 69 | const initAction = match(routes, window.location.href + window.location.search) 70 | if (initAction) { 71 | store.dispatch(initAction) 72 | } 73 | 74 | window.store = store 75 | ReactDOM.render( 76 | , 77 | document.getElementById('root') 78 | ) 79 | 80 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/store/connect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default (stateToProps = {}, actions = {}) => (Component) => { 4 | return class extends React.PureComponent { 5 | static contextTypes = { 6 | store: React.PropTypes.object.isRequired, 7 | } 8 | 9 | static displayName = Component.displayName || Component.name 10 | 11 | state = this.context.store.getState() 12 | 13 | componentDidMount() { 14 | this.context.store.listen(this._handleStoreChange) 15 | } 16 | 17 | componentWillUnmout() { 18 | this.context.store.unlisten(this._handleStoreChange) 19 | } 20 | 21 | _handleStoreChange = () => { 22 | this.setState( this.context.store.getState()) 23 | } 24 | 25 | _createAction = (action) => { 26 | const services = this.context.store.getServices() 27 | const requestedServices = (action.services || []).reduce((providedServices, serviceName) => { 28 | return { 29 | ...providedServices, 30 | [serviceName]: services[serviceName], 31 | } 32 | }, {}) 33 | 34 | return (...args) => { 35 | this.context.store.dispatch(action(...(args.concat([requestedServices])))) 36 | } 37 | } 38 | 39 | _prepareActions() { 40 | const actionKeys = Object.keys(actions) 41 | return actionKeys.reduce((wrappedActions, actionKey) => { 42 | wrappedActions[actionKey] = this._createAction(actions[actionKey]) 43 | 44 | return wrappedActions 45 | }, {}) 46 | } 47 | 48 | render() { 49 | const stateKeys = Object.keys(stateToProps) 50 | const state = stateKeys.reduce((state, stateKey) => { 51 | return { 52 | ...state, 53 | [stateToProps[stateKey]]: this.state[stateKey], // transform to keys from stateProps = {'stateKey': 'propsKey'} 54 | } 55 | }, stateKeys.length === 0 ? this.state : {}) 56 | 57 | const props = { 58 | ...state, 59 | ...this.props, 60 | ...this._prepareActions(), 61 | } 62 | 63 | return 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/store/index.js: -------------------------------------------------------------------------------- 1 | import connectReact from './connect' 2 | export const connect = connectReact 3 | 4 | 5 | export const initialData = { 6 | vatRatio: 21, 7 | invoices: [{ 8 | 'id': '2017/1', 9 | 'customer': 'Avocode Inc.', 10 | 'price': 123000, 11 | 'VAT': 25830, 12 | 'total': 148830, 13 | }], 14 | tax: { 15 | income: 123000, 16 | costsRatio: 60, 17 | taxRatio: 15, 18 | tax: 7380, 19 | }, 20 | nextInvoice: { 21 | }, 22 | user: { 23 | username: '', 24 | id: 0, 25 | email: '', 26 | } 27 | } 28 | 29 | 30 | export const applyMiddleWare = (middlewares) => (createStore) => { 31 | return (...args) => { 32 | const store = createStore(...args) 33 | 34 | const dispatch = middlewares.reduce((originalDispatch, middleware) => { 35 | return middleware(store)(originalDispatch) 36 | }, store.dispatch) 37 | 38 | return { 39 | ...store, 40 | dispatch, 41 | } 42 | } 43 | } 44 | 45 | 46 | export default (initialState, initialReducers, services = {}) => { 47 | let listeners = [] 48 | let state = initialState 49 | const reducers = initialReducers 50 | 51 | const listen = (listener) => { 52 | listeners.push(listener) 53 | } 54 | 55 | const unlisten = (listener) => { 56 | listeners = listeners.filter(candidate => candidate !== listener) 57 | } 58 | 59 | const getServices = () => { 60 | return services 61 | } 62 | 63 | const getState = (keys = []) => { 64 | return keys.reduce((state, key) => { 65 | return { 66 | [key]: state[key], 67 | } 68 | }, state) 69 | } 70 | 71 | const dispatch = (action) => { 72 | const nextState = reducers.reduce((reducedState, reducer) => { 73 | return reducer(reducedState, action) 74 | }, state) 75 | if (nextState === state) { 76 | return 77 | } 78 | 79 | state = nextState 80 | listeners.forEach(listener => listener()) 81 | return state 82 | } 83 | 84 | services = { 85 | ...services, 86 | getState, 87 | dispatch, 88 | } 89 | 90 | 91 | return { 92 | listen, 93 | unlisten, 94 | dispatch, 95 | getState, 96 | getServices, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/store/logger-middleware.js: -------------------------------------------------------------------------------- 1 | 2 | export default (dev = true) => (store) => (next) => { 3 | const logIt = (title, toLog) => { 4 | if (!dev) { 5 | return 6 | } 7 | 8 | console.log(title, toLog) 9 | } 10 | 11 | return (action) => { 12 | const nextState = next(action) 13 | logIt('Action:', action) 14 | logIt('State:', nextState) 15 | return nextState 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/store/mini-redux.js: -------------------------------------------------------------------------------- 1 | 2 | const createDispatch = (initialState, reducers, onChange) => { 3 | return (action) => { 4 | const nextState = reducers.reduce((reducedState, reducer) => { 5 | const st = reducer(reducedState, action) 6 | console.log('st', st) 7 | return st 8 | }, initialState) 9 | 10 | onChange(nextState, createDispatch(nextState, reducers, onChange)) 11 | } 12 | } 13 | 14 | const renderApp = (state, dispatch) => { 15 | console.log(state) 16 | window.dispatch = dispatch 17 | return 18 | } 19 | 20 | const reducer = (state) => { 21 | return { 22 | ...state, 23 | actionCounter: state.actionCounter + 1, 24 | } 25 | } 26 | 27 | const dispatch = createDispatch({ 'actionCounter': 0 }, [reducer], renderApp) 28 | 29 | window.dispatch = dispatch 30 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/store/reducer.js: -------------------------------------------------------------------------------- 1 | export default (state, { action, payload }) => { 2 | switch (action) { 3 | case 'INVOICE_MODAL_OPEN': 4 | return { 5 | ...state, 6 | nextInvoice: { 7 | id: payload['id'], 8 | } 9 | } 10 | 11 | case 'INVOICE_MODAL_CLOSE': 12 | return { 13 | ...state, 14 | nextInvoice: {} 15 | } 16 | 17 | case 'UPDATE_NEXT_INVOICE': 18 | return { 19 | ...state, 20 | nextInvoice: { 21 | ...(state.nextInvoice || {}), 22 | [payload['invoiceProperty']]: payload['value'], 23 | } 24 | } 25 | 26 | 27 | case 'ADD_NEXT_INVOICE': 28 | return { 29 | ...state, 30 | nextInvoice: {}, 31 | invoices: state.invoices.concat([payload.invoice]) 32 | } 33 | } 34 | 35 | return state 36 | } 37 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/store/store-provider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class StoreProvider extends React.PureComponent { 4 | static childContextTypes = { 5 | store: React.PropTypes.object.isRequired, 6 | database: React.PropTypes.object.isRequired, 7 | } 8 | 9 | getChildContext() { 10 | return { 11 | store: this.props.store, 12 | database: this.props.database, 13 | } 14 | } 15 | 16 | render() { 17 | return this.props.children 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pt.6-React-advanced-redux/src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | export const snapShotToArray = snapshot => { 3 | const val = snapshot.val() || {} 4 | return Object.keys(val).map(id => { 5 | return { 6 | ...val[id], 7 | _id: id 8 | } 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /styleguide/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /styleguide/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /styleguide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styleguide", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "false": "0.0.4", 7 | "react": "^15.4.2", 8 | "react-dom": "^15.4.2" 9 | }, 10 | "devDependencies": { 11 | "react-scripts": "0.9.3" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | } 19 | } -------------------------------------------------------------------------------- /styleguide/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdev-js-evenings/react-workshop/31a0a48f1a5419003195abbb8dc19f90e88f4a62/styleguide/public/favicon.ico -------------------------------------------------------------------------------- /styleguide/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | React App 18 | 19 | 20 |
    21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /styleguide/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /styleguide/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import ReactDOMServer from 'react-dom/server' 4 | import Form from './components/form' 5 | import { getFormFieldsFromPropTypes } from './utils' 6 | 7 | 8 | class ComponentContainer extends Component { 9 | state = {} 10 | 11 | _handleInputChange = (propName, value) => { 12 | const obj = {} 13 | obj[propName] = value 14 | 15 | const stateUpdate = Object.assign({}, this.state, obj) 16 | this.setState(stateUpdate) 17 | } 18 | 19 | render() { 20 | const formFields = getFormFieldsFromPropTypes(this.props.component) 21 | const component = 22 | 23 | return ( 24 |
    25 |

    {this.props.component.name || this.props.component.type}

    26 |
    27 |
    28 |
    29 | 30 | 31 | </code> 32 | <div className="col-md-5"> 33 | {component} 34 | </div> 35 | </div>) 36 | } 37 | } 38 | 39 | 40 | class App extends Component { 41 | render() { 42 | return ( 43 | <div className="container"> 44 | <h1>Styleguide</h1> 45 | {this.props.components.map(Component => { 46 | return <ComponentContainer component={Component} /> 47 | })} 48 | </div> 49 | ); 50 | } 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /styleguide/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(<App />, div); 8 | }); 9 | -------------------------------------------------------------------------------- /styleguide/src/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | const Button = (props) => { 5 | return ( 6 | <button className={props.className}>{`Text: ${props.text}`}</button> 7 | ) 8 | } 9 | Button.propTypes = { 10 | className: React.PropTypes.string, 11 | text: React.PropTypes.string, 12 | } 13 | 14 | 15 | 16 | export default Button 17 | -------------------------------------------------------------------------------- /styleguide/src/components/form.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | const FormField = (props) => { 5 | const field = props.field 6 | 7 | function handleInput(e) { 8 | if (field.type === 'checkbox') { 9 | 10 | props.onFieldChange(field.name, e.target.checked) 11 | return 12 | } 13 | 14 | props.onFieldChange(field.name, e.target.value) 15 | } 16 | 17 | return ( 18 | <input 19 | className="form-control" 20 | type={field.type} 21 | name={field.name} 22 | onChange={handleInput} 23 | id={field.id}/> 24 | ) 25 | } 26 | 27 | 28 | export default (props) => { 29 | function handleKeyPress(e) { 30 | console.log(e.target.name) 31 | } 32 | 33 | return ( 34 | <div> 35 | {props.fields.map(field => { 36 | return ( 37 | <div className="form-group"> 38 | <label htmlFor={field.id}>{field.name}</label> 39 | <FormField field={field} onFieldChange={props.onFieldChange}/> 40 | </div> 41 | ) 42 | })} 43 | </div> 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /styleguide/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /styleguide/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import Button from './components/button' 5 | import './index.css'; 6 | 7 | const Pozdrav = (props) => { 8 | return ( 9 | <h2>{`Pozdrav: ${props.name || 'Nikoho'}` }</h2> 10 | ) 11 | } 12 | Pozdrav.propTypes = { 13 | name: React.PropTypes.string, 14 | } 15 | 16 | 17 | const DropDown = (props) => { 18 | console.log(props) 19 | return ( 20 | <div className="btn-group btn-dropdown"> 21 | <button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 22 | Action <span className="caret"></span> 23 | </button> 24 | <ul className={`dropdown-menu ${props.collapsed ? 'hidden' : 'show'}`}> 25 | <li><a href="#">Action</a></li> 26 | <li><a href="#">Another action</a></li> 27 | <li><a href="#">Something else here</a></li> 28 | <li role="separator" className="divider"></li> 29 | <li><a href="#">Separated link</a></li> 30 | </ul> 31 | </div> 32 | ) 33 | } 34 | DropDown.propTypes = { 35 | collapsed: React.PropTypes.bool, 36 | } 37 | 38 | 39 | 40 | const appState = { 41 | components: [Button, DropDown, Pozdrav], 42 | } 43 | 44 | 45 | 46 | 47 | 48 | ReactDOM.render( 49 | <App {...appState} />, 50 | document.getElementById('root') 51 | ); 52 | -------------------------------------------------------------------------------- /styleguide/src/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> 2 | <g fill="#61DAFB"> 3 | <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> 4 | <circle cx="420.9" cy="296.5" r="45.7"/> 5 | <path d="M520.5 78.1z"/> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /styleguide/src/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | const getInputTypeOfPropType = (propType) => { 5 | switch (propType) { 6 | case React.PropTypes.number: 7 | return 'number' 8 | 9 | case React.PropTypes.bool: 10 | return 'checkbox' 11 | 12 | case React.PropTypes.array: 13 | return 'array' 14 | 15 | default: 16 | return 'text' 17 | } 18 | } 19 | 20 | export const getFormFieldsFromPropTypes = (component) => { 21 | return Object.keys(component.propTypes || {}).map(key => { 22 | const propType = component.propTypes[key] 23 | return { 24 | type: getInputTypeOfPropType(propType), 25 | name: key, 26 | id: `${component.type || component.name}__${key}`, 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.3: 6 | version "1.3.3" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | ansi-regex@^2.0.0: 13 | version "2.1.1" 14 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 15 | 16 | ansi-styles@^2.2.1: 17 | version "2.2.1" 18 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 19 | 20 | array-flatten@1.1.1: 21 | version "1.1.1" 22 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 23 | 24 | asap@~2.0.3: 25 | version "2.0.5" 26 | resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" 27 | 28 | base64url@2.0.0, base64url@^2.0.0: 29 | version "2.0.0" 30 | resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" 31 | 32 | buffer-equal-constant-time@1.0.1: 33 | version "1.0.1" 34 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 35 | 36 | builtins@^1.0.3: 37 | version "1.0.3" 38 | resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" 39 | 40 | chalk@^1.1.1: 41 | version "1.1.3" 42 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 43 | dependencies: 44 | ansi-styles "^2.2.1" 45 | escape-string-regexp "^1.0.2" 46 | has-ansi "^2.0.0" 47 | strip-ansi "^3.0.0" 48 | supports-color "^2.0.0" 49 | 50 | commander@^2.9.0: 51 | version "2.9.0" 52 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 53 | dependencies: 54 | graceful-readlink ">= 1.0.0" 55 | 56 | content-disposition@0.5.2: 57 | version "0.5.2" 58 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 59 | 60 | content-type@~1.0.2: 61 | version "1.0.2" 62 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 63 | 64 | cookie-signature@1.0.6: 65 | version "1.0.6" 66 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 67 | 68 | cookie@0.3.1: 69 | version "0.3.1" 70 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 71 | 72 | core-js@^1.0.0: 73 | version "1.2.7" 74 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" 75 | 76 | create-react-app@^1.2.0: 77 | version "1.2.0" 78 | resolved "https://registry.yarnpkg.com/create-react-app/-/create-react-app-1.2.0.tgz#19f634e36afda9b0923330f82a8dc485dbc6b055" 79 | dependencies: 80 | chalk "^1.1.1" 81 | commander "^2.9.0" 82 | cross-spawn "^4.0.0" 83 | fs-extra "^1.0.0" 84 | semver "^5.0.3" 85 | validate-npm-package-name "^3.0.0" 86 | 87 | cross-spawn@^4.0.0: 88 | version "4.0.2" 89 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" 90 | dependencies: 91 | lru-cache "^4.0.1" 92 | which "^1.2.9" 93 | 94 | debug@~2.2.0: 95 | version "2.2.0" 96 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 97 | dependencies: 98 | ms "0.7.1" 99 | 100 | depd@~1.1.0: 101 | version "1.1.0" 102 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 103 | 104 | destroy@~1.0.4: 105 | version "1.0.4" 106 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 107 | 108 | dom-storage@^2.0.2: 109 | version "2.0.2" 110 | resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.0.2.tgz#ed17cbf68abd10e0aef8182713e297c5e4b500b0" 111 | 112 | ecdsa-sig-formatter@1.0.9: 113 | version "1.0.9" 114 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" 115 | dependencies: 116 | base64url "^2.0.0" 117 | safe-buffer "^5.0.1" 118 | 119 | ee-first@1.1.1: 120 | version "1.1.1" 121 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 122 | 123 | encodeurl@~1.0.1: 124 | version "1.0.1" 125 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 126 | 127 | encoding@^0.1.11: 128 | version "0.1.12" 129 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 130 | dependencies: 131 | iconv-lite "~0.4.13" 132 | 133 | escape-html@~1.0.3: 134 | version "1.0.3" 135 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 136 | 137 | escape-string-regexp@^1.0.2: 138 | version "1.0.5" 139 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 140 | 141 | etag@~1.7.0: 142 | version "1.7.0" 143 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.7.0.tgz#03d30b5f67dd6e632d2945d30d6652731a34d5d8" 144 | 145 | express@^4.14.1: 146 | version "4.14.1" 147 | resolved "https://registry.yarnpkg.com/express/-/express-4.14.1.tgz#646c237f766f148c2120aff073817b9e4d7e0d33" 148 | dependencies: 149 | accepts "~1.3.3" 150 | array-flatten "1.1.1" 151 | content-disposition "0.5.2" 152 | content-type "~1.0.2" 153 | cookie "0.3.1" 154 | cookie-signature "1.0.6" 155 | debug "~2.2.0" 156 | depd "~1.1.0" 157 | encodeurl "~1.0.1" 158 | escape-html "~1.0.3" 159 | etag "~1.7.0" 160 | finalhandler "0.5.1" 161 | fresh "0.3.0" 162 | merge-descriptors "1.0.1" 163 | methods "~1.1.2" 164 | on-finished "~2.3.0" 165 | parseurl "~1.3.1" 166 | path-to-regexp "0.1.7" 167 | proxy-addr "~1.1.3" 168 | qs "6.2.0" 169 | range-parser "~1.2.0" 170 | send "0.14.2" 171 | serve-static "~1.11.2" 172 | type-is "~1.6.14" 173 | utils-merge "1.0.0" 174 | vary "~1.1.0" 175 | 176 | faye-websocket@0.9.3: 177 | version "0.9.3" 178 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.3.tgz#482a505b0df0ae626b969866d3bd740cdb962e83" 179 | dependencies: 180 | websocket-driver ">=0.5.1" 181 | 182 | fbjs@^0.8.9: 183 | version "0.8.9" 184 | resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14" 185 | dependencies: 186 | core-js "^1.0.0" 187 | isomorphic-fetch "^2.1.1" 188 | loose-envify "^1.0.0" 189 | object-assign "^4.1.0" 190 | promise "^7.1.1" 191 | setimmediate "^1.0.5" 192 | ua-parser-js "^0.7.9" 193 | 194 | finalhandler@0.5.1: 195 | version "0.5.1" 196 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.1.tgz#2c400d8d4530935bc232549c5fa385ec07de6fcd" 197 | dependencies: 198 | debug "~2.2.0" 199 | escape-html "~1.0.3" 200 | on-finished "~2.3.0" 201 | statuses "~1.3.1" 202 | unpipe "~1.0.0" 203 | 204 | firebase@^4.1.2: 205 | version "4.1.2" 206 | resolved "https://registry.yarnpkg.com/firebase/-/firebase-4.1.2.tgz#b213ab229eb65e31bdbd1d262059b1bb1edfac1f" 207 | dependencies: 208 | dom-storage "^2.0.2" 209 | faye-websocket "0.9.3" 210 | jsonwebtoken "^7.3.0" 211 | promise-polyfill "^6.0.2" 212 | xmlhttprequest "^1.8.0" 213 | 214 | forwarded@~0.1.0: 215 | version "0.1.0" 216 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 217 | 218 | fresh@0.3.0: 219 | version "0.3.0" 220 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f" 221 | 222 | fs-extra@^1.0.0: 223 | version "1.0.0" 224 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" 225 | dependencies: 226 | graceful-fs "^4.1.2" 227 | jsonfile "^2.1.0" 228 | klaw "^1.0.0" 229 | 230 | graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: 231 | version "4.1.11" 232 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 233 | 234 | "graceful-readlink@>= 1.0.0": 235 | version "1.0.1" 236 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 237 | 238 | has-ansi@^2.0.0: 239 | version "2.0.0" 240 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 241 | dependencies: 242 | ansi-regex "^2.0.0" 243 | 244 | hoek@2.x.x: 245 | version "2.16.3" 246 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 247 | 248 | http-errors@~1.5.1: 249 | version "1.5.1" 250 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" 251 | dependencies: 252 | inherits "2.0.3" 253 | setprototypeof "1.0.2" 254 | statuses ">= 1.3.1 < 2" 255 | 256 | iconv-lite@~0.4.13: 257 | version "0.4.15" 258 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" 259 | 260 | inherits@2.0.3: 261 | version "2.0.3" 262 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 263 | 264 | ipaddr.js@1.2.0: 265 | version "1.2.0" 266 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.2.0.tgz#8aba49c9192799585bdd643e0ccb50e8ae777ba4" 267 | 268 | is-stream@^1.0.1: 269 | version "1.1.0" 270 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 271 | 272 | isemail@1.x.x: 273 | version "1.2.0" 274 | resolved "https://registry.yarnpkg.com/isemail/-/isemail-1.2.0.tgz#be03df8cc3e29de4d2c5df6501263f1fa4595e9a" 275 | 276 | isexe@^1.1.1: 277 | version "1.1.2" 278 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" 279 | 280 | isomorphic-fetch@^2.1.1: 281 | version "2.2.1" 282 | resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" 283 | dependencies: 284 | node-fetch "^1.0.1" 285 | whatwg-fetch ">=0.10.0" 286 | 287 | joi@^6.10.1: 288 | version "6.10.1" 289 | resolved "https://registry.yarnpkg.com/joi/-/joi-6.10.1.tgz#4d50c318079122000fe5f16af1ff8e1917b77e06" 290 | dependencies: 291 | hoek "2.x.x" 292 | isemail "1.x.x" 293 | moment "2.x.x" 294 | topo "1.x.x" 295 | 296 | js-tokens@^3.0.0: 297 | version "3.0.1" 298 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" 299 | 300 | jsonfile@^2.1.0: 301 | version "2.4.0" 302 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" 303 | optionalDependencies: 304 | graceful-fs "^4.1.6" 305 | 306 | jsonwebtoken@^7.3.0: 307 | version "7.4.1" 308 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz#7ca324f5215f8be039cd35a6c45bb8cb74a448fb" 309 | dependencies: 310 | joi "^6.10.1" 311 | jws "^3.1.4" 312 | lodash.once "^4.0.0" 313 | ms "^2.0.0" 314 | xtend "^4.0.1" 315 | 316 | jwa@^1.1.4: 317 | version "1.1.5" 318 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" 319 | dependencies: 320 | base64url "2.0.0" 321 | buffer-equal-constant-time "1.0.1" 322 | ecdsa-sig-formatter "1.0.9" 323 | safe-buffer "^5.0.1" 324 | 325 | jws@^3.1.4: 326 | version "3.1.4" 327 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" 328 | dependencies: 329 | base64url "^2.0.0" 330 | jwa "^1.1.4" 331 | safe-buffer "^5.0.1" 332 | 333 | klaw@^1.0.0: 334 | version "1.3.1" 335 | resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" 336 | optionalDependencies: 337 | graceful-fs "^4.1.9" 338 | 339 | lodash.once@^4.0.0: 340 | version "4.1.1" 341 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 342 | 343 | loose-envify@^1.0.0, loose-envify@^1.1.0: 344 | version "1.3.1" 345 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" 346 | dependencies: 347 | js-tokens "^3.0.0" 348 | 349 | lru-cache@^4.0.1: 350 | version "4.0.2" 351 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" 352 | dependencies: 353 | pseudomap "^1.0.1" 354 | yallist "^2.0.0" 355 | 356 | media-typer@0.3.0: 357 | version "0.3.0" 358 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 359 | 360 | merge-descriptors@1.0.1: 361 | version "1.0.1" 362 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 363 | 364 | methods@~1.1.2: 365 | version "1.1.2" 366 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 367 | 368 | mime-db@~1.26.0: 369 | version "1.26.0" 370 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" 371 | 372 | mime-types@~2.1.11, mime-types@~2.1.13: 373 | version "2.1.14" 374 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" 375 | dependencies: 376 | mime-db "~1.26.0" 377 | 378 | mime@1.3.4: 379 | version "1.3.4" 380 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 381 | 382 | moment@2.x.x: 383 | version "2.18.1" 384 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" 385 | 386 | ms@0.7.1: 387 | version "0.7.1" 388 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 389 | 390 | ms@0.7.2: 391 | version "0.7.2" 392 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 393 | 394 | ms@^2.0.0: 395 | version "2.0.0" 396 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 397 | 398 | negotiator@0.6.1: 399 | version "0.6.1" 400 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 401 | 402 | node-fetch@^1.0.1: 403 | version "1.6.3" 404 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.6.3.tgz#dc234edd6489982d58e8f0db4f695029abcd8c04" 405 | dependencies: 406 | encoding "^0.1.11" 407 | is-stream "^1.0.1" 408 | 409 | object-assign@^4.1.0: 410 | version "4.1.1" 411 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 412 | 413 | on-finished@~2.3.0: 414 | version "2.3.0" 415 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 416 | dependencies: 417 | ee-first "1.1.1" 418 | 419 | parseurl@~1.3.1: 420 | version "1.3.1" 421 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 422 | 423 | path-to-regexp@0.1.7: 424 | version "0.1.7" 425 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 426 | 427 | promise-polyfill@^6.0.2: 428 | version "6.0.2" 429 | resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.0.2.tgz#d9c86d3dc4dc2df9016e88946defd69b49b41162" 430 | 431 | promise@^7.1.1: 432 | version "7.1.1" 433 | resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf" 434 | dependencies: 435 | asap "~2.0.3" 436 | 437 | prop-types@^15.5.7, prop-types@~15.5.7: 438 | version "15.5.8" 439 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" 440 | dependencies: 441 | fbjs "^0.8.9" 442 | 443 | proxy-addr@~1.1.3: 444 | version "1.1.3" 445 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" 446 | dependencies: 447 | forwarded "~0.1.0" 448 | ipaddr.js "1.2.0" 449 | 450 | pseudomap@^1.0.1: 451 | version "1.0.2" 452 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 453 | 454 | qs@6.2.0: 455 | version "6.2.0" 456 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" 457 | 458 | range-parser@~1.2.0: 459 | version "1.2.0" 460 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 461 | 462 | react-dom@^15.5.4: 463 | version "15.5.4" 464 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da" 465 | dependencies: 466 | fbjs "^0.8.9" 467 | loose-envify "^1.1.0" 468 | object-assign "^4.1.0" 469 | prop-types "~15.5.7" 470 | 471 | react@^15.5.4: 472 | version "15.5.4" 473 | resolved "https://registry.yarnpkg.com/react/-/react-15.5.4.tgz#fa83eb01506ab237cdc1c8c3b1cea8de012bf047" 474 | dependencies: 475 | fbjs "^0.8.9" 476 | loose-envify "^1.1.0" 477 | object-assign "^4.1.0" 478 | prop-types "^15.5.7" 479 | 480 | safe-buffer@^5.0.1: 481 | version "5.1.0" 482 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" 483 | 484 | semver@^5.0.3: 485 | version "5.3.0" 486 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 487 | 488 | send@0.14.2: 489 | version "0.14.2" 490 | resolved "https://registry.yarnpkg.com/send/-/send-0.14.2.tgz#39b0438b3f510be5dc6f667a11f71689368cdeef" 491 | dependencies: 492 | debug "~2.2.0" 493 | depd "~1.1.0" 494 | destroy "~1.0.4" 495 | encodeurl "~1.0.1" 496 | escape-html "~1.0.3" 497 | etag "~1.7.0" 498 | fresh "0.3.0" 499 | http-errors "~1.5.1" 500 | mime "1.3.4" 501 | ms "0.7.2" 502 | on-finished "~2.3.0" 503 | range-parser "~1.2.0" 504 | statuses "~1.3.1" 505 | 506 | serve-static@~1.11.2: 507 | version "1.11.2" 508 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.11.2.tgz#2cf9889bd4435a320cc36895c9aa57bd662e6ac7" 509 | dependencies: 510 | encodeurl "~1.0.1" 511 | escape-html "~1.0.3" 512 | parseurl "~1.3.1" 513 | send "0.14.2" 514 | 515 | setimmediate@^1.0.5: 516 | version "1.0.5" 517 | resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" 518 | 519 | setprototypeof@1.0.2: 520 | version "1.0.2" 521 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" 522 | 523 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 524 | version "1.3.1" 525 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 526 | 527 | strip-ansi@^3.0.0: 528 | version "3.0.1" 529 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 530 | dependencies: 531 | ansi-regex "^2.0.0" 532 | 533 | supports-color@^2.0.0: 534 | version "2.0.0" 535 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 536 | 537 | topo@1.x.x: 538 | version "1.1.0" 539 | resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5" 540 | dependencies: 541 | hoek "2.x.x" 542 | 543 | type-is@~1.6.14: 544 | version "1.6.14" 545 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" 546 | dependencies: 547 | media-typer "0.3.0" 548 | mime-types "~2.1.13" 549 | 550 | ua-parser-js@^0.7.9: 551 | version "0.7.12" 552 | resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb" 553 | 554 | unpipe@~1.0.0: 555 | version "1.0.0" 556 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 557 | 558 | utils-merge@1.0.0: 559 | version "1.0.0" 560 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 561 | 562 | validate-npm-package-name@^3.0.0: 563 | version "3.0.0" 564 | resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" 565 | dependencies: 566 | builtins "^1.0.3" 567 | 568 | vary@~1.1.0: 569 | version "1.1.0" 570 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" 571 | 572 | websocket-driver@>=0.5.1: 573 | version "0.6.5" 574 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" 575 | dependencies: 576 | websocket-extensions ">=0.1.1" 577 | 578 | websocket-extensions@>=0.1.1: 579 | version "0.1.1" 580 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.1.tgz#76899499c184b6ef754377c2dbb0cd6cb55d29e7" 581 | 582 | whatwg-fetch@>=0.10.0: 583 | version "2.0.2" 584 | resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e" 585 | 586 | which@^1.2.9: 587 | version "1.2.12" 588 | resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" 589 | dependencies: 590 | isexe "^1.1.1" 591 | 592 | xmlhttprequest@^1.8.0: 593 | version "1.8.0" 594 | resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" 595 | 596 | xtend@^4.0.1: 597 | version "4.0.1" 598 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 599 | 600 | yallist@^2.0.0: 601 | version "2.0.0" 602 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.0.0.tgz#306c543835f09ee1a4cb23b7bce9ab341c91cdd4" 603 | --------------------------------------------------------------------------------