├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── assets └── react_ujs.js ├── composer.json ├── config └── config.php ├── facades └── ReactFacade.php ├── install_v8js.md ├── lib ├── React.php └── ReactServiceProvider.php ├── phpunit.php ├── phpunit.xml ├── tests ├── PrerenderTest.php ├── WrapperTagTest.php └── fixtures │ └── components.js └── travis.php.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | node_modules/ 4 | package.json 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | 8 | before_install: 9 | - sudo apt-get update 10 | - sudo apt-get install libv8-dev g++ cpp valgrind libxml2-dev -y 11 | - printf "\n" | pecl install v8js-0.1.3 12 | - phpenv config-add travis.php.ini 13 | - travis_retry composer self-update 14 | 15 | install: 16 | - travis_retry composer install --no-interaction --prefer-source 17 | 18 | script: valgrind --trace-children=yes vendor/bin/phpunit --debug --verbose 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.10 (2015-07-13) 2 | 3 | Features: 4 | 5 | - Components now can be namespaced 6 | 7 | Updates: 8 | 9 | - Update React from 0.13.1 to 0.13.3 10 | 11 | # 0.9.1 (2015-06-11) 12 | 13 | Fixes: 14 | 15 | - Blade directive now renders properly again 16 | 17 | # 0.9 (2015-05-27) 18 | 19 | Features: 20 | 21 | - The `ReactServiceProvider` is now deferred. 22 | - Configs now are on a separated file from `config/app.php`, it's `config/react.php` now. 23 | 24 | # 0.8 (2015-05-09) 25 | 26 | Features: 27 | 28 | - React and component's source are now cached at production. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/talyssonoc/react-laravel/badges/gpa.svg)](https://codeclimate.com/github/talyssonoc/react-laravel) [![Build Status](https://travis-ci.org/talyssonoc/react-laravel.svg?branch=master)](https://travis-ci.org/talyssonoc/react-laravel) 2 | 3 | # react-laravel 4 | 5 | With `react-laravel` you'll be able to use [ReactJS](https://facebook.github.io/react/) components right from your Blade views, with optional server-side rendering, and use them on the client-side with React due to unobtrusive JavaScript. 6 | 7 | # Installation 8 | 9 | ## V8js dependency 10 | 11 | It's important to know that `react-laravel` has an indirect dependency of the [v8js](https://pecl.php.net/package/v8js) PHP extension. 12 | 13 | You can see how to install it here: [how to install v8js](install_v8js.md). 14 | 15 | ## Composer 16 | 17 | Set the `minimum-stability` of your `composer.json` to `dev`, adding this: 18 | 19 | ```json 20 | "minimum-stability": "dev" 21 | ``` 22 | 23 | Then run: 24 | 25 | ```sh 26 | $ composer require talyssonoc/react-laravel:0.11 27 | ``` 28 | 29 | After that you should add this to your providers at the `config/app.php` file of your Laravel app: 30 | 31 | ```php 32 | 'React\ReactServiceProvider' 33 | ``` 34 | 35 | And then run: 36 | 37 | ```sh 38 | php artisan vendor:publish 39 | ``` 40 | 41 | And the `react.php` file will be available at the `config` folder of your app. 42 | 43 | # Usage 44 | 45 | After the installation and configuration, you'll be able to use the `@react_component` directive in your views. 46 | 47 | The `@react_component` directive accepts 3 arguments: 48 | 49 | ```php 50 | @react_component([, props, options]) 51 | 52 | //example 53 | @react_component('Message', [ 'title' => 'Hello, World' ], [ 'prerender' => true ]) 54 | 55 | // example using namespaced component 56 | @react_component('Acme.Message', [ 'title' => 'Hello, World' ], [ 'prerender' => true ]) 57 | ``` 58 | 59 | * `componentName`: Is the name of the global variable that holds your component. When using [Namespaced Components](https://facebook.github.io/react/docs/jsx-in-depth.html#namespaced-components) you may use dot-notation for the component name. 60 | * `props`: Associative of the `props` that'll be passed to your component 61 | * `options`: Associative array of options that you can pass to the `react-laravel`: 62 | * `prerender`: Tells react-laravel to render your component server-side, and then just _mount_ it on the client-side. Default to __true__. 63 | * `tag`: The tag of the element that'll hold your component. Default to __'div'__. 64 | * _html attributes_: Any other valid HTML attribute that will be added to the wrapper element of your component. Example: `'id' => 'my_component'`. 65 | 66 | All your components should be inside `public/js/components.js` (you can configure it, see below) and be global. 67 | 68 | You must include `react.js`, `react-dom.js` and `react_ujs.js` (in this order) in your view. You can concatenate these files together using laravel-elixir. 69 | 70 | `react-laravel` provides a ReactJS installation and the `react_us.js` file, they'll be at `public/vendor/react-laravel` folder after you install `react-laravel` and run: 71 | 72 | ```sh 73 | $ php artisan vendor:publish --force 74 | ``` 75 | 76 | For using the files provided by `react-laravel` and your `components.js` file, add this to your view: 77 | 78 | ```html 79 | 80 | 81 | 82 | 83 | ``` 84 | 85 | If you'll use a different version from the one provided by react-laravel (see `composer.json`), you got to configure it (see below). 86 | 87 | # Configurations 88 | 89 | You can change settings to `react-laravel` at the `config/react.php` file: 90 | 91 | ```php 92 | return [ 93 | 'source' => 'path_for_react.js', 94 | 'dom-source' => 'path_for_react-dom.js', 95 | 'dom-server-source' => 'path_for_react-dom-server.js', 96 | 'components' => [ 'path_for_file_containing_your_components.js' ] 97 | ]; 98 | ``` 99 | 100 | All of them are optional. 101 | 102 | * `source`: defaults to `public/vendor/react-laravel/react.js`. 103 | * `dom-source`: defaults to `public/vendor/react-laravel/react-dom.js`. 104 | * `dom-server-source`: defaults to `public/vendor/react-laravel/react-dom-server.js`. 105 | * `components`: defaults to `public/js/components.js`. Multiple components files may be specified here. 106 | 107 | Your `components.js` file(s) should also be included at your view, and all your components must be at the `window` object. 108 | 109 | # Thanks 110 | 111 | This package is inspired at [react-rails](https://github.com/reactjs/react-rails). 112 | -------------------------------------------------------------------------------- /assets/react_ujs.js: -------------------------------------------------------------------------------- 1 | ;(function(document, window) { 2 | 3 | window.ReactLaravelUJS = { 4 | CLASS_NAME_ATTR: 'data-react-class', 5 | PROPS_ATTR: 'data-react-props', 6 | 7 | getElements: function getElements() { 8 | var finder = function finder(selector) { 9 | if(typeof jQuery === 'undefined') { 10 | return document.querySelectorAll(selector); 11 | } 12 | else { 13 | return jQuery(selector); 14 | } 15 | }; 16 | 17 | return finder('[' + ReactLaravelUJS.CLASS_NAME_ATTR + ']'); 18 | }, 19 | 20 | mountComponents: function mountComponents() { 21 | var elements = ReactLaravelUJS.getElements(); 22 | var element; 23 | var reactClass; 24 | var props; 25 | 26 | var index = function index(obj, i) { 27 | return obj[i]; 28 | }; 29 | 30 | for(var i = 0; i < elements.length; i++) { 31 | element = elements[i]; 32 | reactClass = element.getAttribute(ReactLaravelUJS.CLASS_NAME_ATTR).split('.').reduce(index, window); 33 | props = JSON.parse(element.getAttribute(ReactLaravelUJS.PROPS_ATTR)); 34 | 35 | ReactDOM.render(React.createElement(reactClass, props), element); 36 | } 37 | }, 38 | 39 | unmountComponents: function unmountComponents() { 40 | var elements = ReactLaravelUJS.getElements(); 41 | 42 | for(var i = 0; i < elements.length; i++) { 43 | ReactDOM.unmountComponentAtNode(elements[i]); 44 | } 45 | }, 46 | 47 | handleEvents: function handleEvents() { 48 | if (document.readyState == "complete" || document.readyState == "loaded" || document.readyState == "interactive") { 49 | ReactLaravelUJS.mountComponents(); 50 | } 51 | else { 52 | document.addEventListener('DOMContentLoaded', ReactLaravelUJS.mountComponents); 53 | } 54 | window.addEventListener('unload', ReactLaravelUJS.unmountComponents); 55 | } 56 | }; 57 | 58 | ReactLaravelUJS.handleEvents(); 59 | 60 | })(document, window); 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "talyssonoc/react-laravel", 3 | "description": "Package to use ReactJS with Laravel", 4 | "license": "MIT", 5 | "keywords": ["react", "reactjs", "laravel"], 6 | "require": { 7 | "illuminate/support": "~5.0", 8 | "reactjs/react-php-v8js": "dev-master", 9 | "koala-framework/composer-extra-assets": "~1.1" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "~4.0.0@stable" 13 | }, 14 | "extra": { 15 | "require-npm": { 16 | "react": "^0.14.6", 17 | "react-dom": "^0.14.6" 18 | } 19 | }, 20 | "authors": [ 21 | { 22 | "name": "Talysson Oliveira", 23 | "email": "talyssonoc@gmail.com" 24 | } 25 | ], 26 | "autoload": { 27 | "classmap": [ 28 | "facades/" 29 | ], 30 | "psr-4": { 31 | "React\\": "lib/" 32 | } 33 | }, 34 | "minimum-stability": "dev" 35 | } 36 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | public_path('vendor/react-laravel/react.js'), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | React-Dom Source 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Path to the react-dom.js file. 22 | | 23 | */ 24 | 25 | 'dom-source' => public_path('vendor/react-laravel/react-dom.js'), 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | React-Dom-Server Source 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Path to the react-dom-server.js file. 33 | | 34 | */ 35 | 36 | 'dom-server-source' => public_path('vendor/react-laravel/react-dom-server.js'), 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Components 41 | |-------------------------------------------------------------------------- 42 | | 43 | | An array of paths to the javascript files containing your components. 44 | | 45 | */ 46 | 47 | 'components' => [ 48 | public_path('js/components.js') 49 | ] 50 | ]; 51 | -------------------------------------------------------------------------------- /facades/ReactFacade.php: -------------------------------------------------------------------------------- 1 | reactSource = $reactSource; 21 | $this->componentsSource = $componentsSource; 22 | $this->defaultOptions = [ 23 | 'prerender' => true, 24 | 'tag' => 'div' 25 | ]; 26 | } 27 | 28 | /** 29 | * Returns the ReactJS serverside rendering instance associated with this 30 | * object. 31 | * @return ReactJS The instance, if it does not exist yet, it will be created 32 | */ 33 | private function getReact () { 34 | if ($this->react === null) { 35 | $this->react = new \ReactJS($this->reactSource, $this->componentsSource); 36 | } 37 | return $this->react; 38 | } 39 | 40 | /** 41 | * Render a ReactJS component 42 | * @param string $component Name of the component object 43 | * @param array $props Associative array of props of the component 44 | * @param array $options Associative array of options of rendering 45 | * @return string Markup of the rendered component 46 | */ 47 | public function render($component, $props = null, $options = []) { 48 | $options = array_merge($this->defaultOptions, $options); 49 | $tag = $options['tag']; 50 | $markup = ''; 51 | 52 | // Creates the markup of the component 53 | if ($options['prerender'] === true) { 54 | $markup = $this->getReact()->setComponent($component, $props)->getMarkup(); 55 | } 56 | 57 | // Pass props back to view as value of `data-react-props` 58 | $props = htmlentities(json_encode($props), ENT_QUOTES); 59 | 60 | // Gets all values that aren't used as options and map it as HTML attributes 61 | $htmlAttributes = array_diff_key($options, $this->defaultOptions); 62 | $htmlAttributesString = $this->arrayToHTMLAttributes($htmlAttributes); 63 | 64 | return "<{$tag} data-react-class='{$component}' data-react-props='{$props}' {$htmlAttributesString}>{$markup}"; 65 | } 66 | 67 | /** 68 | * Convert associative array to string of HTML attributes 69 | * @param array $array Associative array of attributes 70 | * @return string 71 | */ 72 | private function arrayToHTMLAttributes($array) { 73 | $htmlAttributesString = ''; 74 | foreach ($array as $attribute => $value) { 75 | $htmlAttributesString .= "{$attribute}='{$value}'"; 76 | } 77 | return $htmlAttributesString; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/ReactServiceProvider.php: -------------------------------------------------------------------------------- 1 | createMatcher('react_component'); 12 | 13 | return preg_replace($pattern, '', $view); 14 | }); 15 | 16 | $prev = __DIR__ . '/../'; 17 | 18 | $this->publishes([ 19 | $prev . 'assets' => public_path('vendor/react-laravel'), 20 | $prev . 'node_modules/react/dist' => public_path('vendor/react-laravel'), 21 | $prev . 'node_modules/react-dom/dist' => public_path('vendor/react-laravel'), 22 | ], 'assets'); 23 | 24 | $this->publishes([ 25 | $prev . 'config/config.php' => config_path('react.php'), 26 | ], 'config'); 27 | } 28 | 29 | public function register() { 30 | 31 | $this->app->bind('React', function() { 32 | $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'react'); 33 | 34 | $reactBaseSource = file_get_contents(config('react.source')); 35 | $reactDomSource = file_get_contents(config('react.dom-source')); 36 | $reactDomServerSource = file_get_contents(config('react.dom-server-source')); 37 | 38 | $componentsSource = null; 39 | $components = config('react.components'); 40 | if (!is_array($components)) { 41 | $components = [$components]; 42 | } 43 | foreach ($components as $component) { 44 | $componentsSource .= file_get_contents($component); 45 | } 46 | 47 | $reactSource = $reactBaseSource; 48 | $reactSource .= $reactDomSource; 49 | $reactSource .= $reactDomServerSource; 50 | return new React($reactSource, $componentsSource); 51 | }); 52 | } 53 | 54 | protected function createMatcher($function) { 55 | return '/(?loadHTML($string, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 21 | $element = $document->childNodes->item(0); 22 | 23 | return $element; 24 | } 25 | 26 | public static function innerHTML($node) { 27 | $document = new DOMDocument(); 28 | $node = $document->importNode($node, true); 29 | $document->appendChild($node); 30 | return $document->saveHTML($node); 31 | } 32 | 33 | public static function removeAttributes($node) { 34 | $newNode = $node->cloneNode(true); 35 | 36 | if($node->hasAttributes()) { 37 | $attributes = $node->attributes; 38 | foreach ($attributes as $i => $attr){ 39 | $newNode->removeAttribute($attr->name); 40 | } 41 | } 42 | 43 | if($newNode->hasChildNodes()) { 44 | $childNodes = $newNode->childNodes; 45 | for($i = 0; $i < $childNodes->length; $i++) { 46 | $child = $childNodes->item($i); 47 | $newNode->replaceChild(TestHelpers::removeAttributes($child), $child); 48 | } 49 | } 50 | 51 | return $newNode; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/PrerenderTest.php: -------------------------------------------------------------------------------- 1 | react = new React($GLOBALS['reactSource'], $GLOBALS['componentsSource']); 11 | } 12 | 13 | public function testWithoutPrerender() { 14 | $elementString = $this->react->render('Alert', null, [ 'prerender' => false ]); 15 | $wrapperElement = TestHelpers::stringToElement($elementString); 16 | 17 | $this->assertFalse($wrapperElement->hasChildNodes()); 18 | } 19 | 20 | public function testWithPrerender() { 21 | $elementString = $this->react->render('Alert', [ 'name' => 'react-laravel' ], [ 'prerender' => true ]); 22 | $wrapperElement = TestHelpers::stringToElement($elementString); 23 | 24 | $this->assertEquals('Hello, react-laravel', $wrapperElement->textContent); 25 | 26 | $expectedElement = TestHelpers::stringToElement('
Hello, react-laravel
'); 27 | $elementWithoutAttributes = TestHelpers::removeAttributes($wrapperElement->childNodes->item(0)); 28 | 29 | $this->assertEquals(TestHelpers::innerHTML($expectedElement), 30 | TestHelpers::innerHTML($elementWithoutAttributes)); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/WrapperTagTest.php: -------------------------------------------------------------------------------- 1 | react = new React($GLOBALS['reactSource'], $GLOBALS['componentsSource']); 11 | } 12 | 13 | public function testDataAttributesWithoutProps() { 14 | $elementString = $this->react->render('Alert'); 15 | $element = TestHelpers::stringToElement($elementString); 16 | 17 | $this->assertEquals('Alert', $element->getAttribute('data-react-class')); 18 | $this->assertEquals('null', $element->getAttribute('data-react-props')); 19 | } 20 | 21 | public function testDataAttributesWithProps() { 22 | $props = [ 'name' => 'react-laravel' ]; 23 | $elementString = $this->react->render('Alert', $props); 24 | $element = TestHelpers::stringToElement($elementString); 25 | 26 | $this->assertEquals('Alert', $element->getAttribute('data-react-class')); 27 | $this->assertEquals(json_encode($props), 28 | $element->getAttribute('data-react-props')); 29 | } 30 | 31 | public function testDataAttributesWithMoreProps() { 32 | $props = [ 33 | 'name' => 'react-laravel', 34 | 'anotherProp' => 'value' 35 | ]; 36 | 37 | $elementString = $this->react->render('Alert', $props); 38 | 39 | $element = TestHelpers::stringToElement($elementString); 40 | 41 | $this->assertEquals('Alert', $element->getAttribute('data-react-class')); 42 | $this->assertEquals(json_encode($props), 43 | $element->getAttribute('data-react-props')); 44 | } 45 | 46 | public function testDefaultWrapperTagName() { 47 | $elementString = $this->react->render('Alert'); 48 | $element = TestHelpers::stringToElement($elementString); 49 | 50 | $this->assertEquals('div', $element->tagName); 51 | } 52 | 53 | public function testCustomWrapperTagName() { 54 | $elementString = $this->react->render('Alert', null, [ 'tag' => 'span' ]); 55 | $element = TestHelpers::stringToElement($elementString); 56 | 57 | $this->assertEquals('span', $element->tagName); 58 | } 59 | 60 | public function testHTMLAttributes() { 61 | 62 | $attributes = [ 63 | 'id' => 'react-laravel', 64 | 'class' => 'react_laravel_class', 65 | 'tag' => 'span' 66 | ]; 67 | 68 | $elementString = $this->react->render('Alert', null, $attributes); 69 | 70 | $element = TestHelpers::stringToElement($elementString); 71 | 72 | $this->assertTrue($element->hasAttributes()); 73 | $this->assertEquals('react-laravel', $element->getAttribute('id')); 74 | $this->assertEquals('react_laravel_class', $element->getAttribute('class')); 75 | $this->assertEquals('', $element->getAttribute('tag')); 76 | $this->assertEquals('', $element->getAttribute('prerender')); 77 | } 78 | } -------------------------------------------------------------------------------- /tests/fixtures/components.js: -------------------------------------------------------------------------------- 1 | var Alert = React.createClass({displayName: 'Alert', 2 | render: function() { 3 | return React.createElement('div', null, 'Hello, ', 4 | React.createElement('strong', null, this.props.name) 5 | ); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /travis.php.ini: -------------------------------------------------------------------------------- 1 | extension=v8js.so 2 | --------------------------------------------------------------------------------