├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── example ├── controllers │ └── SiteController.php └── views │ ├── test.php │ └── vueapp │ └── test │ ├── css │ └── test.css │ └── js │ └── test.js ├── phpunit.xml.dist ├── src ├── VueApp.php └── assets │ └── VueAsset.php └── tests ├── ExampleTest.php ├── TestCase.php ├── assets └── .gitignore ├── bootstrap.php ├── compatibility.php └── views ├── layouts └── main.php └── site ├── example1-js-ready.php ├── example1.php └── vueapp ├── example1-js-ready └── js │ └── app.js └── example1 └── js └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | /composer.lock 19 | 20 | # composer itself is not needed 21 | composer.phar 22 | 23 | # Mac DS_Store Files 24 | .DS_Store 25 | 26 | # phpunit itself is not needed 27 | phpunit.phar 28 | # local phpunit config 29 | /phpunit.xml 30 | # phpunit cache 31 | .phpunit.result.cache 32 | 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - php: "7.3" 7 | 8 | - php: "7.2" 9 | 10 | - php: "7.1" 11 | 12 | - php: "7.0" 13 | 14 | - php: "5.6" 15 | 16 | sudo: false 17 | 18 | # cache vendor dirs 19 | cache: 20 | directories: 21 | - $HOME/.composer/cache 22 | 23 | install: 24 | - travis_retry composer self-update && composer --version 25 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 26 | - travis_retry composer install --prefer-dist --no-interaction 27 | 28 | before_script: 29 | - | 30 | if [ $TRAVIS_PHP_VERSION = '5.6' ]; then 31 | PHPUNIT_FLAGS="--coverage-clover=coverage.clover" 32 | fi 33 | 34 | script: 35 | - ./vendor/bin/phpunit --verbose $PHPUNIT_FLAGS 36 | 37 | after_script: 38 | - | 39 | if [ $TRAVIS_PHP_VERSION = '5.6' ]; then 40 | travis_retry wget https://scrutinizer-ci.com/ocular.phar 41 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover 42 | fi -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Fabrizio Caldarelli 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vue.js helper for Yii2 2 | ===================== 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/fabriziocaldarelli/yii2-vueapp/v/stable)](https://packagist.org/packages/fabriziocaldarelli/yii2-vueapp) 5 | [![Total Downloads](https://poser.pugx.org/fabriziocaldarelli/yii2-vueapp/downloads)](https://packagist.org/packages/fabriziocaldarelli/yii2-vueapp) 6 | [![Build Status](https://travis-ci.org/FabrizioCaldarelli/yii2-vueapp.svg?branch=master)](https://travis-ci.org/FabrizioCaldarelli/yii2-vueapp) 7 | 8 | This is a component that helps to create Vue.js app without usign webpack or similar. 9 | 10 | All assets (js, css and templates) are injected directly in the html and this components 11 | provides functionalities to split the code (js, css and templates) and to load parameters 12 | from html root element. 13 | 14 | Two default packages are embedded with this component: Axios and Moment. There is an example 15 | that shows how use both in the code. 16 | 17 | Installation 18 | ------------ 19 | 20 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 21 | 22 | Either run 23 | 24 | ``` 25 | php composer.phar require --prefer-dist fabriziocaldarelli/yii2-vueapp "^1.0" 26 | ``` 27 | 28 | or add 29 | 30 | ``` 31 | "fabriziocaldarelli/yii2-vueapp": "^1.0" 32 | ``` 33 | 34 | to the require section of your `composer.json` file. 35 | 36 | How it works 37 | ------------ 38 | 39 | This component injects js, css and tpl (php or html) files into the returned html. 40 | 41 | These files are read starting from same folder of action view file, appending vueapp/*actionName*/js or vueapp/*actionName*/css or vueapp/*actionName*/tpl 42 | 43 | VueApp::begin mainly supports three parameters: 44 | 45 | - id: vue app html tag id selector; 46 | - propsData: widget uses this element to pass data from html/php to js script; 47 | - packages: list of packages that should be loaded into js vue script 48 | 49 | **Pay attention**: *propsData* keys have the same name (and same case) in php and in js file. 50 | 51 | Usage 52 | ----- 53 | 54 | **1) The view file** 55 | 56 | Inside the view, call VueApp widget: 57 | 58 | ```php 59 | 'vueAppTest', 73 | 'propsData' => [ 74 | 'kParam1' => 'value_1', 75 | 'kParam2' => 'value_2', 76 | 'kParam3' => 'value_3', 77 | 'kParamObj' => ['a' => 10], 78 | ], 79 | /* 80 | 'jsFiles' => [ ... ], // list of other js files, that have precedente over js contents path files 81 | 'cssFiles' => [ ... ], // list of other css files, that have precedente over css contents path files 82 | 'tplFiles' => [ ... ], // list of other tpl files, that have precedente over tpl contents path files 83 | */ 84 | 'assets' => [ 85 | \sfmobile\vueapp\assets\axios\AxiosAsset::class, 86 | \sfmobile\vueapp\assets\moment\MomentAsset::class, 87 | \sfmobile\vueapp\assets\vue_select\VueSelectAsset::class 88 | \sfmobile\vueapp\assets\uid\UivAsset::class 89 | \sfmobile\vueapp\assets\vue_bootstrap_datetime_picker\VueBootstrapDatetimePickerAsset::class 90 | ] 91 | ]); 92 | ?> 93 | kParam1: {{ propsApp.kParam1 }} 94 |
95 | kParam2: {{ propsApp.kParam2 }} 96 |
97 | kParam3: {{ propsApp.kParam3 }} 98 |
99 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 100 |
101 | 102 | 103 |
104 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 105 |
106 | 107 | 108 | 109 | becomes 136 | 137 | // Vue Select asset - To avoid conflicts: 138 | // Vue.component('v-select', VueSelect.VueSelect); 139 | 140 | var ___VUEAPP_APP_ID___ = new Vue({ 141 | el: '#___VUEAPP_APP_ID___', 142 | 143 | // If you need a date picker, 144 | // add VueApp::PKG_VUEJS_DATEPICKER to 'packages' VueApp widget config 145 | // Refer to https://github.com/charliekassel/vuejs-datepicker 146 | components: { 147 | vuejsDatepicker, // using VueJsDatePicker 148 | "date-picker": VueBootstrapDatetimePicker, // using VueBootstrapDatetimePicker - https://github.com/ankurk91/vue-bootstrap-datetimepicker 149 | 'v-select' : VueSelect.VueSelect // using VueSelect - https://vue-select.org/guide/install.html#yarn-npm 150 | }, 151 | 152 | data: { 153 | 154 | /** 155 | * propsApp is used to collect attribute related to root container element. 156 | * This is the suggested way to pass data from php to js vue app. 157 | * All parameter are converted from dash to camel case (html k-param-1 become kParam1) 158 | */ 159 | propsApp: { 160 | kParam1: null, 161 | kParam2: null, 162 | kParam3: null, 163 | kParamObj: null, 164 | }, 165 | 166 | clock_datetime: null, 167 | 168 | datePickerValue: null, 169 | 170 | vueBootstrapDatetimePickerOptions: { 171 | // https://momentjs.com/docs/#/displaying/ 172 | format: "DD/MM/YYYY HH:mm", 173 | locale: 'it', 174 | useCurrent: false, 175 | showClear: true, 176 | showClose: true 177 | } 178 | }, 179 | 180 | filters: { 181 | formatDateTime: function (value, format) { 182 | return value ? moment(value).format(format) : null 183 | } 184 | }, 185 | 186 | mounted() { 187 | this.readPropsApp(); 188 | 189 | // Because kParamObj is an object, we have to parse to serialized version of kParamObj 190 | this.propsApp.kParamObj = JSON.parse(this.propsApp.kParamObj); 191 | 192 | this.loadAtomicClock(); 193 | }, 194 | 195 | methods: { 196 | 197 | readPropsApp: function () { 198 | for (var k in this.propsApp) { 199 | 200 | // Taken from: https://github.com/sindresorhus/decamelize/blob/master/index.js 201 | var attr = k 202 | .replace(/([a-z\d])([A-Z])/g, '$1-$2') 203 | .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2') 204 | .toLowerCase(); 205 | 206 | console.log(k, attr); 207 | if (this.$el.attributes[attr] != undefined) { 208 | this.propsApp[k] = this.$el.attributes[attr].value; 209 | } 210 | } 211 | } 212 | 213 | loadAtomicClock: function () { 214 | 215 | var self = this; 216 | 217 | axios 218 | .get('http://worldtimeapi.org/api/ip') 219 | .then(function (response) { 220 | console.log(response); 221 | self.clock_datetime = response.data.datetime; 222 | }) 223 | .catch(error => console.log(error)); 224 | }, 225 | 226 | } 227 | }) 228 | ``` 229 | 230 | **3) The Vue app css files** 231 | 232 | Starting from view path folder, css files for app and components are in vueapp/test/css/*actionName*/ . 233 | 234 | For example, the path for main vue app css could be vueapp/test/css/test.css 235 | 236 | ```css 237 | [v-cloak] { 238 | display: none 239 | } 240 | ``` 241 | 242 | Tips & Tricks 243 | ----- 244 | 245 |

1. Pass data from html/php to js

246 | To pass data from html/php to js vue app, I used an attribute called propsApp, whithin are defined attributes passed in html root element. 247 | 248 | For example, html root element "vueAppTest": 249 | 250 | ```html 251 |
256 | ... 257 |
258 | ``` 259 | 260 | all parameters defined in data.propsApp are readed from html when app is mounted (calling readPropsApp method): 261 | 262 | ```js 263 | var vueAppTest = new Vue({ 264 | el: '#vueAppTest', 265 | data: { 266 | 267 | /** 268 | * propsApp is used to collect attribute related to root container element. 269 | * This is the suggested way to pass data from php to js vue app. 270 | * All parameter are converted from dash to camel case (html k-param-1 become kParam1) 271 | */ 272 | propsApp: { 273 | kParam1: null, 274 | kParam2: null, 275 | kParam3: null, 276 | }, 277 | 278 | }, 279 | 280 | mounted() { 281 | this.readPropsApp(); 282 | }, 283 | 284 | methods: { 285 | 286 | readPropsApp: function () { 287 | for (var k in this.propsApp) { 288 | 289 | // Taken from: https://github.com/sindresorhus/decamelize/blob/master/index.js 290 | var attr = k 291 | .replace(/([a-z\d])([A-Z])/g, '$1-$2') 292 | .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2') 293 | .toLowerCase(); 294 | 295 | console.log(k, attr); 296 | if (this.$el.attributes[attr] != undefined) { 297 | this.propsApp[k] = this.$el.attributes[attr].value; 298 | } 299 | } 300 | }, 301 | } 302 | } 303 | ``` 304 | 305 |

2. Component registration

306 | 307 | JS files loading order is important when the app js file depends from other js files. 308 | 309 | So, I suggest to prefix all component files with '_' or suffix with '.component.' in order to load component js files firstly. 310 | 311 |

3. Pass object data from html/php to js

312 | Passing objects/array from html/php to js is the same of primitive dat (Tips and triks #1). 313 | 314 | The only different thing is that in mounted() function you need to parse json string to the object. 315 | 316 | So, if kObject is the object passed from php, mounted() method will be: 317 | 318 | ```js 319 | mounted() { 320 | this.readPropsApp(); 321 | 322 | // Because kParamObj is an object, we have to parse to serialized version of kParamObj 323 | this.propsApp.kParamObj = JSON.parse(this.propsApp.kParamObj); 324 | }, 325 | ``` 326 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabriziocaldarelli/yii2-vueapp", 3 | "description": "A Vue.js helper for create Vue app in Yii2 without webpack or similar", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension","vue"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Fabrizio Caldarelli", 10 | "email": "fabrizio.caldarelli@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "npm-asset/vue": "^2.6" 15 | }, 16 | "require-dev": { 17 | "yiisoft/yii2": "~2.0", 18 | "yiisoft/yii2-coding-standards": "~2.0", 19 | "phpunit/phpunit": "<7" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "sfmobile\\vueapp\\": "src" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "sfmobile_unit\\vueapp\\": "tests" 29 | } 30 | }, 31 | "repositories": [ 32 | { 33 | "type": "composer", 34 | "url": "https://asset-packagist.org" 35 | } 36 | ], 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "1.0.x-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | render('test'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/views/test.php: -------------------------------------------------------------------------------- 1 | 'vueAppTest', 16 | 'propsData' => [ 17 | 'kParam1' => 'value_1', 18 | 'kParam2' => 'value_2', 19 | 'kParam3' => 'value_3', 20 | 'kParamObj' => ['a' => 10], 21 | ], 22 | /* 23 | 'jsFiles' => [ ... ], // list of other js files, that have precedente over js contents path files 24 | 'cssFiles' => [ ... ], // list of other css files, that have precedente over css contents path files 25 | 'tplFiles' => [ ... ], // list of other tpl files, that have precedente over tpl contents path files 26 | */ 27 | 'assets' => [ 28 | \sfmobile\vueapp\assets\axios\AxiosAsset::class, 29 | \sfmobile\vueapp\assets\moment\MomentAsset::class, 30 | \sfmobile\vueapp\assets\vuejs_datepicker\VueJsDatepickerAsset::class, 31 | \sfmobile\vueapp\assets\uiv\UivAsset::class, 32 | \sfmobile\vueapp\assets\vue_select\VueSelectAsset::class, 33 | ] 34 | ]); 35 | ?> 36 | kParam1: {{ propsApp.kParam1 }} 37 |
38 | kParam2: {{ propsApp.kParam2 }} 39 |
40 | kParam3: {{ propsApp.kParam3 }} 41 |
42 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 43 |
44 | 45 | 46 |
47 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 48 | 49 | -------------------------------------------------------------------------------- /example/views/vueapp/test/css/test.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none 3 | } -------------------------------------------------------------------------------- /example/views/vueapp/test/js/test.js: -------------------------------------------------------------------------------- 1 | var ___VUEAPP_APP_ID___ = new Vue({ 2 | el: '#___VUEAPP_APP_ID___', 3 | 4 | // If you need a date picker, 5 | // add VueApp::PKG_VUEJS_DATEPICKER to 'packages' VueApp widget config 6 | // Refer to https://github.com/charliekassel/vuejs-datepicker 7 | components: { 8 | vuejsDatepicker 9 | }, 10 | 11 | data: { 12 | 13 | /** 14 | * propsApp is used to collect attribute related to root container element. 15 | * This is the suggested way to pass data from php to js vue app. 16 | * All parameter are converted from dash to camel case (html k-param-1 become kParam1) 17 | */ 18 | propsApp: { 19 | kParam1: null, 20 | kParam2: null, 21 | kParam3: null, 22 | kParamObj: null, 23 | }, 24 | 25 | clock_datetime: null 26 | }, 27 | 28 | filters: { 29 | formatDateTime: function (value, format) { 30 | return value ? moment(value).format(format) : null 31 | } 32 | }, 33 | 34 | mounted() { 35 | this.readPropsApp(); 36 | 37 | // Because kParamObj is an object, we have to parse to serialized version of kParamObj 38 | this.propsApp.kParamObj = JSON.parse(this.propsApp.kParamObj); 39 | 40 | this.loadAtomicClock(); 41 | }, 42 | 43 | methods: { 44 | 45 | readPropsApp: function () { 46 | for (var k in this.propsApp) { 47 | 48 | // Taken from: https://github.com/sindresorhus/decamelize/blob/master/index.js 49 | var attr = k 50 | .replace(/([a-z\d])([A-Z])/g, '$1-$2') 51 | .replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2') 52 | .toLowerCase(); 53 | 54 | console.log(k, attr); 55 | if (this.$el.attributes[attr] != undefined) { 56 | this.propsApp[k] = this.$el.attributes[attr].value; 57 | } 58 | } 59 | }, 60 | 61 | loadAtomicClock: function () { 62 | 63 | var self = this; 64 | 65 | axios 66 | .get('http://worldtimeapi.org/api/ip') 67 | .then(function (response) { 68 | console.log(response); 69 | self.clock_datetime = response.data.datetime; 70 | }) 71 | .catch(error => console.log(error)); 72 | }, 73 | 74 | } 75 | }) -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | ./tests 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/VueApp.php: -------------------------------------------------------------------------------- 1 | /js, vueapp//tpl, vueapp//css folders 20 | */ 21 | class VueApp extends Widget 22 | { 23 | /** 24 | * @deprecated Use assets property: php composer.phar require --prefer-dist fabriziocaldarelli/yii2-vueapp-axios "@dev" 25 | */ 26 | const PKG_AXIOS = 'axios'; 27 | /** 28 | * @deprecated Use assets property: php composer.phar require --prefer-dist fabriziocaldarelli/yii2-vueapp-moment "@dev" 29 | */ 30 | const PKG_MOMENT = 'moment'; 31 | /** 32 | * @deprecated Use assets property: php composer.phar require --prefer-dist fabriziocaldarelli/yii2-vueapp-vuejs-datepicker "@dev" 33 | */ 34 | const PKG_VUEJS_DATEPICKER = 'vuejs_datepicker'; 35 | /** 36 | * @deprecated Use assets property: php composer.phar require --prefer-dist fabriziocaldarelli/yii2-vueapp-uiv "@dev" 37 | */ 38 | const PKG_UIV = 'uiv'; 39 | /** 40 | * @deprecated Use assets property: php composer.phar require --prefer-dist fabriziocaldarelli/yii2-vueapp-vue-bootstrap-datetime-picker "@dev" 41 | */ 42 | const PKG_VUE_BOOTSTRAP_DATETIME_PICKER = 'vue_bootstrap_datetime_picker'; 43 | /** 44 | * @deprecated Use assets property: https://github.com/FabrizioCaldarelli/yii2-vueapp-select 45 | */ 46 | const PKG_VUE_SELECT = 'vue_select'; 47 | 48 | /** 49 | * id of vue app 50 | */ 51 | public $id = null; 52 | 53 | /** 54 | * tag of vue app container 55 | */ 56 | public $tag = 'div'; 57 | 58 | /** 59 | * add v-cloak options to tag vue app container 60 | */ 61 | public $vCloak = true; 62 | 63 | /** 64 | * props data to pass to js script 65 | */ 66 | public $propsData = []; 67 | 68 | /** 69 | * other options appended to tag vue app container 70 | */ 71 | public $options = []; 72 | 73 | /** 74 | * path of /js, /css and /tpl files 75 | */ 76 | public $contentsPath = null; 77 | 78 | /** 79 | * js files passed from user 80 | */ 81 | public $jsFiles = []; 82 | 83 | /** 84 | * css files passed from user 85 | */ 86 | public $cssFiles = []; 87 | 88 | /** 89 | * tpl files passed from user 90 | */ 91 | public $tplFiles = []; 92 | 93 | /** 94 | * @deprecated Use assets property 95 | * packages to load from assets 96 | */ 97 | public $packages = [ self::PKG_AXIOS ]; 98 | 99 | /** 100 | * assets to load 101 | */ 102 | public $assets = []; 103 | 104 | /** 105 | * Position registration js file. Default is View::POS_READY 106 | */ 107 | public $positionJs = \yii\web\View::POS_READY; 108 | 109 | /** 110 | * debug mode 111 | */ 112 | public $debug = false; 113 | 114 | /** 115 | * js files from contents path 116 | */ 117 | private $contentsPathJsFiles = null; 118 | 119 | /** 120 | * tpl files from contents path 121 | */ 122 | private $contentsPathTplFiles = null; 123 | 124 | /** 125 | * css files from contents path 126 | */ 127 | private $contentsPathCssFiles = null; 128 | 129 | private function checkInit() 130 | { 131 | if($this->id == null) 132 | { 133 | throw new \Exception("Missing 'id' parameter"); 134 | } 135 | 136 | if($this->contentsPath == null) 137 | { 138 | throw new \Exception("Missing contentsPath (usually __DIR__)"); 139 | } 140 | else 141 | { 142 | if(file_exists($this->contentsPath) == false) 143 | { 144 | throw new \Exception( sprintf("contentsPath %s does not exist", $this->contentsPath) ); 145 | } 146 | } 147 | 148 | } 149 | 150 | public function init() 151 | { 152 | parent::init(); 153 | 154 | if($this->contentsPath == null) 155 | { 156 | $folderName = basename($this->view->viewFile, '.php'); 157 | $this->contentsPath = sprintf('%s/vueapp/%s', dirname($this->view->viewFile), $folderName); 158 | } 159 | 160 | $this->checkInit(); 161 | 162 | if(($this->assets != null)&&(is_array($this->assets))&&(count($this->assets)>0)) 163 | { 164 | foreach($this->assets as $asset) 165 | { 166 | $asset::register($this->view); 167 | } 168 | } 169 | else 170 | { 171 | // Load packages 172 | if(in_array(self::PKG_AXIOS, $this->packages)) \sfmobile\vueapp\assets\axios\AxiosAsset::register($this->view); 173 | if(in_array(self::PKG_MOMENT, $this->packages)) \sfmobile\vueapp\assets\moment\MomentAsset::register($this->view); 174 | if(in_array(self::PKG_VUEJS_DATEPICKER, $this->packages)) \sfmobile\vueapp\assets\vuejs_datepicker\VueJsDatepickerAsset::register($this->view); 175 | if(in_array(self::PKG_UIV, $this->packages)) \sfmobile\vueapp\assets\uiv\UivAsset::register($this->view); 176 | if(in_array(self::PKG_VUE_BOOTSTRAP_DATETIME_PICKER, $this->packages)) \sfmobile\vueapp\assets\vue_bootstrap_datetime_picker\VueBootstrapDatetimePickerAsset::register($this->view); 177 | if(in_array(self::PKG_VUE_SELECT, $this->packages)) \sfmobile\vueapp\assets\vue_select\VueSelectAsset::register($this->view); 178 | } 179 | 180 | VueAsset::register($this->view); 181 | 182 | $this->loadFilesContentsPath(); 183 | 184 | ob_start(); 185 | } 186 | 187 | public function run() 188 | { 189 | $outContent = ''; 190 | 191 | // Prepare js files 192 | foreach ($this->jsFiles as $jsFile) { 193 | $jsContent = $this->replaceJsTokens(file_get_contents(\Yii::getAlias($jsFile))); 194 | $this->view->registerJs($jsContent, $this->positionJs); 195 | } 196 | foreach ($this->contentsPathJsFiles as $jsFile) { 197 | $jsContent = $this->replaceJsTokens(file_get_contents(\Yii::getAlias($jsFile))); 198 | $this->view->registerJs($jsContent, $this->positionJs); 199 | } 200 | 201 | // Prepare css files 202 | foreach ($this->cssFiles as $cssFile) { 203 | $cssContent = file_get_contents(\Yii::getAlias($cssFile)); 204 | $this->view->registerCss($cssContent); 205 | } 206 | foreach ($this->contentsPathCssFiles as $cssFile) { 207 | $cssContent = file_get_contents(\Yii::getAlias($cssFile)); 208 | $this->view->registerCss($cssContent); 209 | } 210 | 211 | // Prepare template files 212 | foreach ($this->tplFiles as $tplFile) { 213 | $tplName = pathinfo($tplFile, PATHINFO_FILENAME); 214 | 215 | $tplContent = $this->view->renderFile($tplFile); 216 | $outContent .= Html::tag('script', $tplContent, ['type' => 'text/x-template', 'id' => $tplName]); 217 | } 218 | foreach ($this->contentsPathTplFiles as $tplFile) { 219 | $tplName = pathinfo($tplFile, PATHINFO_FILENAME); 220 | 221 | $tplContent = $this->view->renderFile($tplFile); 222 | $outContent .= Html::tag('script', $tplContent, ['type' => 'text/x-template', 'id' => $tplName]); 223 | } 224 | 225 | // Get inside widget content, between begin() ... end() method 226 | $insideWidgetContent = ob_get_clean(); 227 | 228 | // Add options to html tag 229 | $htmlTagOptions = ['id' => $this->id]; 230 | foreach($this->propsData as $key => $val) 231 | { 232 | $htmlTagOptions[\yii\helpers\Inflector::camel2id($key)] = $val; 233 | } 234 | if($this->vCloak) $htmlTagOptions['v-cloak'] = ''; 235 | $htmlTagOptions = array_merge($htmlTagOptions, $this->options); 236 | 237 | // Fill output content 238 | $outContent .= Html::beginTag($this->tag, $htmlTagOptions); 239 | $outContent .= $insideWidgetContent; 240 | $outContent .= Html::endTag($this->tag); 241 | 242 | return $outContent; 243 | } 244 | 245 | /** 246 | * Replace all tokens occurrences in js files 247 | * Token: 248 | * ___VUE_APP_ID___ : app id 249 | */ 250 | private function replaceJsTokens($content) 251 | { 252 | $content = str_replace('___VUEAPP_APP_ID___', $this->id, $content); 253 | return $content; 254 | } 255 | 256 | private function loadFilesContentsPath() 257 | { 258 | if($this->contentsPath != null) 259 | { 260 | $basePath = \Yii::getAlias($this->contentsPath); 261 | 262 | $jsPath = $basePath.'/js'; 263 | $this->contentsPathJsFiles = $this->loadFilesFromPath($jsPath); 264 | $tplPath = $basePath.'/tpl'; 265 | $this->contentsPathTplFiles = $this->loadFilesFromPath($tplPath); 266 | $cssPath = $basePath.'/css'; 267 | $this->contentsPathCssFiles = $this->loadFilesFromPath($cssPath); 268 | } 269 | } 270 | 271 | private function loadFilesFromPath($path) 272 | { 273 | $arrOut = []; 274 | 275 | if(file_exists($path)) 276 | { 277 | $scandir = scandir($path); 278 | foreach($scandir as $file) 279 | { 280 | $pathFile = sprintf('%s/%s', $path, $file); 281 | if(is_file($pathFile)) $arrOut[] = $pathFile; 282 | } 283 | } 284 | else 285 | { 286 | if($this->debug) 287 | { 288 | throw new \Exception( sprintf('path %s does not exist', $path) ); 289 | } 290 | } 291 | 292 | return $arrOut; 293 | } 294 | 295 | 296 | } 297 | -------------------------------------------------------------------------------- /src/assets/VueAsset.php: -------------------------------------------------------------------------------- 1 | js[] = YII_DEBUG ? 'dist/vue.js' : 'dist/vue.min.js'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/ExampleTest.php: -------------------------------------------------------------------------------- 1 | mockWebApplication([ 12 | 'components' => [ 13 | 'assetManager' => [ 14 | 'class' => \yii\web\AssetManager::class, 15 | 'hashCallback' => function ($path) { 16 | $basePath = realpath(\Yii::$app->basePath . '/../'); 17 | $path = str_replace($basePath, '', $path); 18 | return hash('md5', $path); 19 | } 20 | ] 21 | ] 22 | ]); 23 | } 24 | 25 | /** 26 | * Example without using render. Property 'contentsPath' is passed to the widget 27 | */ 28 | public function testPositionEndWithoutRender() 29 | { 30 | ob_start(); 31 | VueApp::begin([ 32 | 'id' => 'vueAppTest', 33 | 'contentsPath' => __DIR__ . '/views/site', 34 | 'propsData' => [ 35 | 'kParam1' => 'value_1', 36 | 'kParam2' => 'value_2', 37 | 'kParam3' => 'value_3', 38 | 'kParamObj' => ['a' => 10], 39 | ], 40 | /* 41 | 'jsFiles' => [ ... ], // list of other js files, that have precedente over js contents path files 42 | 'cssFiles' => [ ... ], // list of other css files, that have precedente over css contents path files 43 | 'tplFiles' => [ ... ], // list of other tpl files, that have precedente over tpl contents path files 44 | */ 45 | 'packages' => [VueApp::PKG_AXIOS, VueApp::PKG_MOMENT, VueApp::PKG_VUEJS_DATEPICKER, VueApp::PKG_VUE_BOOTSTRAP_DATETIME_PICKER], 46 | 'positionJs' => \yii\web\View::POS_END 47 | ]); 48 | VueApp::end(); 49 | $out = ob_get_clean(); 50 | 51 | $expected = << 53 | HTML; 54 | 55 | 56 | $this->assertEqualsWithoutLE($expected, $out); 57 | 58 | } 59 | 60 | /** 61 | * Example using renderPartial, only view content is rendered 62 | */ 63 | public function testPositionEndRenderPartial() 64 | { 65 | $this->mockAction('site', 'example1'); 66 | $out = \Yii::$app->controller->renderPartial('example1'); 67 | 68 | $expected = <<<'HTML' 69 |
kParam1: {{ propsApp.kParam1 }} 70 |
71 | kParam2: {{ propsApp.kParam2 }} 72 |
73 | kParam3: {{ propsApp.kParam3 }} 74 |
75 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 76 |
77 | 78 | 79 |
80 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 81 |
82 | 83 | 84 |
85 | HTML; 86 | 87 | $this->assertEqualsWithoutLE($expected, $out); 88 | 89 | $this->removeMockedAction(); 90 | } 91 | 92 | 93 | /** 94 | * Example using render, fully template 95 | */ 96 | public function testPositionEndRenderTemplate() 97 | { 98 | $this->mockAction('site', 'example1'); 99 | $out = \Yii::$app->controller->render('example1'); 100 | 101 | $expected = <<<'HTML' 102 | 103 | 104 | 105 | 106 |
kParam1: {{ propsApp.kParam1 }} 107 |
108 | kParam2: {{ propsApp.kParam2 }} 109 |
110 | kParam3: {{ propsApp.kParam3 }} 111 |
112 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 113 |
114 | 115 | 116 |
117 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 118 |
119 | 120 | 121 |
122 | 123 | 124 | 125 | 126 | 202 | 203 | 204 | HTML; 205 | 206 | $this->assertEqualsWithoutLE($expected, $out); 207 | 208 | $this->removeMockedAction(); 209 | 210 | } 211 | 212 | 213 | 214 | /** 215 | * Example using render, fully template 216 | */ 217 | public function testPositionJsReady() 218 | { 219 | $this->mockAction('site', 'example1-js-ready'); 220 | $out = \Yii::$app->controller->render('example1-js-ready'); 221 | 222 | $expected = <<<'HTML' 223 | 224 | 225 | 226 | 227 |
kParam1: {{ propsApp.kParam1 }} 228 |
229 | kParam2: {{ propsApp.kParam2 }} 230 |
231 | kParam3: {{ propsApp.kParam3 }} 232 |
233 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 234 |
235 | 236 | 237 |
238 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 239 |
240 | 241 | 242 |
243 | 244 | 245 | 246 | 247 | 248 | 326 | 327 | 328 | HTML; 329 | 330 | $this->assertEqualsWithoutLE($expected, $out); 331 | 332 | $this->removeMockedAction(); 333 | } 334 | 335 | 336 | } 337 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | mockWebApplication(); 21 | } 22 | 23 | /** 24 | * Clean up after test. 25 | * By default the application created with [[mockApplication]] will be destroyed. 26 | */ 27 | protected function tearDown() 28 | { 29 | parent::tearDown(); 30 | $this->destroyApplication(); 31 | } 32 | 33 | /** 34 | * @param array $config 35 | * @param string $appClass 36 | */ 37 | protected function mockWebApplication($config = [], $appClass = '\yii\web\Application') 38 | { 39 | new $appClass(ArrayHelper::merge([ 40 | 'id' => 'testapp', 41 | 'basePath' => __DIR__, 42 | 'vendorPath' => dirname(__DIR__) . '/vendor', 43 | 'aliases' => [ 44 | '@bower' => '@vendor/bower-asset', 45 | '@npm' => '@vendor/npm-asset', 46 | ], 47 | 'components' => [ 48 | 'request' => [ 49 | 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', 50 | 'scriptFile' => __DIR__ . '/index.php', 51 | 'scriptUrl' => '/index.php', 52 | ], 53 | ] 54 | ], $config)); 55 | } 56 | 57 | /** 58 | * Mocks controller action with parameters 59 | * 60 | * @param string $controllerId 61 | * @param string $actionID 62 | * @param string $moduleID 63 | * @param array $params 64 | */ 65 | protected function mockAction($controllerId, $actionID, $moduleID = null, $params = []) 66 | { 67 | Yii::$app->controller = $controller = new Controller($controllerId, Yii::$app); 68 | $controller->actionParams = $params; 69 | $controller->action = new Action($actionID, $controller); 70 | 71 | if ($moduleID !== null) { 72 | $controller->module = new Module($moduleID); 73 | } 74 | } 75 | 76 | /** 77 | * Removes controller 78 | */ 79 | protected function removeMockedAction() 80 | { 81 | Yii::$app->controller = null; 82 | } 83 | 84 | /** 85 | * Destroys application in Yii::$app by setting it to null. 86 | */ 87 | protected function destroyApplication() 88 | { 89 | Yii::$app = null; 90 | Yii::$container = new Container(); 91 | } 92 | 93 | /** 94 | * Asserting two strings equality ignoring line endings 95 | * 96 | * @param string $expected 97 | * @param string $actual 98 | */ 99 | public function assertEqualsWithoutLE($expected, $actual) 100 | { 101 | $expected = str_replace("\r\n", "\n", $expected); 102 | $actual = str_replace("\r\n", "\n", $actual); 103 | 104 | $this->assertEquals($expected, $actual); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | setExpectedException($exception); 21 | } 22 | 23 | /** 24 | * @param string $message 25 | */ 26 | public function expectExceptionMessage($message) 27 | { 28 | $this->setExpectedException($this->getExpectedException(), $message); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/views/layouts/main.php: -------------------------------------------------------------------------------- 1 | beginPage() ?> 2 | 3 | 4 | head() ?> 5 | 6 | 7 | beginBody() ?> 8 | 9 | endBody() ?> 10 | 11 | 12 | endPage() ?> 13 | -------------------------------------------------------------------------------- /tests/views/site/example1-js-ready.php: -------------------------------------------------------------------------------- 1 | 'vueAppTest', 16 | 'propsData' => [ 17 | 'kParam1' => 'value_1', 18 | 'kParam2' => 'value_2', 19 | 'kParam3' => 'value_3', 20 | 'kParamObj' => ['a' => 10], 21 | ], 22 | /* 23 | 'jsFiles' => [ ... ], // list of other js files, that have precedente over js contents path files 24 | 'cssFiles' => [ ... ], // list of other css files, that have precedente over css contents path files 25 | 'tplFiles' => [ ... ], // list of other tpl files, that have precedente over tpl contents path files 26 | */ 27 | 'packages' => [VueApp::PKG_AXIOS, VueApp::PKG_MOMENT, VueApp::PKG_VUEJS_DATEPICKER, VueApp::PKG_UIV], 28 | ]); 29 | ?> 30 | kParam1: {{ propsApp.kParam1 }} 31 |
32 | kParam2: {{ propsApp.kParam2 }} 33 |
34 | kParam3: {{ propsApp.kParam3 }} 35 |
36 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 37 |
38 | 39 | 40 |
41 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/views/site/example1.php: -------------------------------------------------------------------------------- 1 | 'vueAppTest', 16 | 'propsData' => [ 17 | 'kParam1' => 'value_1', 18 | 'kParam2' => 'value_2', 19 | 'kParam3' => 'value_3', 20 | 'kParamObj' => ['a' => 10], 21 | ], 22 | /* 23 | 'jsFiles' => [ ... ], // list of other js files, that have precedente over js contents path files 24 | 'cssFiles' => [ ... ], // list of other css files, that have precedente over css contents path files 25 | 'tplFiles' => [ ... ], // list of other tpl files, that have precedente over tpl contents path files 26 | */ 27 | 'packages' => [VueApp::PKG_AXIOS, VueApp::PKG_MOMENT, VueApp::PKG_VUEJS_DATEPICKER, VueApp::PKG_UIV], 28 | 'positionJs' => \yii\web\View::POS_END 29 | ]); 30 | ?> 31 | kParam1: {{ propsApp.kParam1 }} 32 |
33 | kParam2: {{ propsApp.kParam2 }} 34 |
35 | kParam3: {{ propsApp.kParam3 }} 36 |
37 | kParamObj: {{ propsApp.kParamObj ? propsApp.kParamObj.a : null }} 38 |
39 | 40 | 41 |
42 | clock datetime: {{ clock_datetime | formatDateTime('DD/MM/YYYY HH:mm') }} 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/views/site/vueapp/example1-js-ready/js/app.js: -------------------------------------------------------------------------------- 1 | var ___VUEAPP_APP_ID___ = new Vue({ 2 | el: '#___VUEAPP_APP_ID___', 3 | 4 | // If you need a date picker, 5 | // add VueApp::PKG_VUEJS_DATEPICKER to 'packages' VueApp widget config 6 | // Refer to https://github.com/charliekassel/vuejs-datepicker 7 | components: { 8 | vuejsDatepicker, 9 | "date-picker": VueBootstrapDatetimePicker 10 | }, 11 | 12 | data: { 13 | 14 | /** 15 | * propsApp is used to collect attribute related to root container element. 16 | * This is the suggested way to pass data from php to js vue app. 17 | * All parameter are converted from dash to camel case (html k-param-1 become kParam1) 18 | */ 19 | propsApp: { 20 | kParam1: null, 21 | kParam2: null, 22 | kParam3: null, 23 | kParamObj: null, 24 | }, 25 | 26 | clock_datetime: null 27 | }, 28 | 29 | filters: { 30 | formatDateTime: function (value, format) { 31 | return value ? moment(value).format(format) : null 32 | } 33 | }, 34 | 35 | mounted() { 36 | this.readPropsApp(); 37 | 38 | // Because kParamObj is an object, we have to parse to serialized version of kParamObj 39 | this.propsApp.kParamObj = JSON.parse(this.propsApp.kParamObj); 40 | 41 | this.loadAtomicClock(); 42 | }, 43 | 44 | methods: { 45 | 46 | readPropsApp: function () { 47 | for (var k in this.propsApp) { 48 | 49 | // Taken from: https://github.com/sindresorhus/decamelize/blob/master/index.js 50 | var attr = k 51 | .replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, `$1-$2`) 52 | .replace(/(\p{Lowercase_Letter}+)(\p{Uppercase_Letter}[\p{Lowercase_Letter}\d]+)/gu, `$1-$2`) 53 | .toLowerCase(); 54 | 55 | console.log(k, attr); 56 | if (this.$el.attributes[attr] != undefined) { 57 | this.propsApp[k] = this.$el.attributes[attr].value; 58 | } 59 | } 60 | }, 61 | 62 | loadAtomicClock: function () { 63 | 64 | var self = this; 65 | 66 | axios 67 | .get('http://worldtimeapi.org/api/ip') 68 | .then(function (response) { 69 | console.log(response); 70 | self.clock_datetime = response.data.datetime; 71 | }) 72 | .catch(error => console.log(error)); 73 | }, 74 | 75 | } 76 | }) -------------------------------------------------------------------------------- /tests/views/site/vueapp/example1/js/app.js: -------------------------------------------------------------------------------- 1 | var ___VUEAPP_APP_ID___ = new Vue({ 2 | el: '#___VUEAPP_APP_ID___', 3 | 4 | // If you need a date picker, 5 | // add VueApp::PKG_VUEJS_DATEPICKER to 'packages' VueApp widget config 6 | // Refer to https://github.com/charliekassel/vuejs-datepicker 7 | components: { 8 | vuejsDatepicker, 9 | "date-picker": VueBootstrapDatetimePicker 10 | }, 11 | 12 | data: { 13 | 14 | /** 15 | * propsApp is used to collect attribute related to root container element. 16 | * This is the suggested way to pass data from php to js vue app. 17 | * All parameter are converted from dash to camel case (html k-param-1 become kParam1) 18 | */ 19 | propsApp: { 20 | kParam1: null, 21 | kParam2: null, 22 | kParam3: null, 23 | kParamObj: null, 24 | }, 25 | 26 | clock_datetime: null 27 | }, 28 | 29 | filters: { 30 | formatDateTime: function (value, format) { 31 | return value ? moment(value).format(format) : null 32 | } 33 | }, 34 | 35 | mounted() { 36 | this.readPropsApp(); 37 | 38 | // Because kParamObj is an object, we have to parse to serialized version of kParamObj 39 | this.propsApp.kParamObj = JSON.parse(this.propsApp.kParamObj); 40 | 41 | this.loadAtomicClock(); 42 | }, 43 | 44 | methods: { 45 | 46 | readPropsApp: function () { 47 | for (var k in this.propsApp) { 48 | 49 | // Taken from: https://github.com/sindresorhus/decamelize/blob/master/index.js 50 | var attr = k 51 | .replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, `$1-$2`) 52 | .replace(/(\p{Lowercase_Letter}+)(\p{Uppercase_Letter}[\p{Lowercase_Letter}\d]+)/gu, `$1-$2`) 53 | .toLowerCase(); 54 | 55 | console.log(k, attr); 56 | if (this.$el.attributes[attr] != undefined) { 57 | this.propsApp[k] = this.$el.attributes[attr].value; 58 | } 59 | } 60 | }, 61 | 62 | loadAtomicClock: function () { 63 | 64 | var self = this; 65 | 66 | axios 67 | .get('http://worldtimeapi.org/api/ip') 68 | .then(function (response) { 69 | console.log(response); 70 | self.clock_datetime = response.data.datetime; 71 | }) 72 | .catch(error => console.log(error)); 73 | }, 74 | 75 | } 76 | }) --------------------------------------------------------------------------------