@BarisLeenders Great inspiration, really!
Unfortunately I had some trouble with your code. But a few modifications later I got it running. The main problem was that I used single file Vue templates (*.vue) with HTML markup and a script section in it instead of defining the template in a .js-file. The .js-files have the backdraw that they have to be included in my mix-file and the HTML markup in the template element of the JS data isn't properly highlighted and formatted by my IDE. So I really want to continue using my .vue-files (which is also recommended).
Thus I applied some modifications to achieve this and gain some comfort, too.
But first some infos about the setup:
php: 7.1.3
laravel/framework: 5.6.34
php-unit/php-unit: 7.3.2
symfony/class-loader: 3.4.14
symfony/*: 4.1.3
vue: 2.5.17
vue-loader: 13.7.3
vue-template-compiler: 2.5.17
bootstrap: 4.1.1
jquery: 3.3.1
laravel-mix: 2.1.14
webpack: 3.12.0
webpack-dev-server: 2.11.3
My approach reduces the number of .js-files needed to only one which registers all the Vue components (of course the .vue-files are still needed).
For an overview I created a list of files you will need to modify or create:
app/OSVue/composers/VueAssetsComposer.php (create - name and path may be changed)
app/OSVue/OSVue.php (create - see above)
ressources/assets/js/components.js (create)
ressources/assets/js/vue.js (create)
ressources/assets/js/app.js (modify)
webpack.mix.js (modify)
ressources/views/layouts/assets.blade.php (create)
ressources/views/layouts/app.blade.php (modify)
Here is my code based on your work:
First the composer (the prefix 'OS' comes from my app, ignore it):
namespace App\OSVue\Composers;
use Illuminate\Support\Facades\View;
class VueAssetsComposer
{
protected $_components = [];
public function __construct($components = [])
{
$this->_components = $components;
}
public function compose()
{
$components = $this->_components;
foreach ($components as $slug => $component) {
if(file_exists(resource_path('assets/js/components/'.$component.'.vue')))
continue;
array_forget($components, $slug);
}
View::composer('layouts.assets',
function( $view ) use ($components){
$view->with('components', $components);
}
);
}
}
The main difference is that the composer now requires an array of key/value pairs. The key defines the 'slug' (or the name to be used in the HTML markup later on) and the value is the name of the .vue-file without suffix .vue. I will show the usage later.
And here is the class file that is to be used in the controller to declare the use of any component (there should be no differences to yours except for naming):
namespace App\OSVue;
use App\OSVue\Composers\VueAssetsComposer;
class OSVue
{
/**
* Laravel application
*
* @var \Illuminate\Foundation\Application
*/
public $app;
/**
* Create a new instance.
*
* @param \Illuminate\Foundation\Application $app
*
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Adds a Vue Component to the view
*
* @param array $components
*/
public static function requireVueComponent($components = [])
{
$c = new VueAssetsComposer($components);
$c->compose();
}
}
In any controller you load components to be used in the view this way (here a snippet from the index method of the home controller):
OSVue::requireVueComponent(['example-component' => 'ExampleComponent', 'example-component2' => 'ExampleComponent2']);
return view('home');
The components then can be included in the corresponding blade file normally by using <example-component></example-component>. The component itself is defined in the file ExampleComponent.vue as recommended by the Vue docs.
Ok, now to the glue: The .js-file that manages the component registration. I named it components.js and placed it where app.js is located. This is it:
$encodedComponents = document.getElementById('include-components').dataset.components;
$components = JSON.parse($encodedComponents);
for (var $slug in $components) {
Vue.component($slug, require('./components/' + $components[$slug] + '.vue'));
}
What does it do? The components to be registered are provided as an HTML data-* option named data-components to the script tag with the id 'include-components' (you will see this later). The data is json encoded which has to be reverted. Well the rest is ... obvious.
You will have to include this .js-file into your mixfile webpack.mix.js together with your app.js and vue.js otherwise it won't work ('require' will fail):
mix.js('resources/assets/js/app.js', 'public/js')
.js('resources/assets/js/components.js', 'public/js')
.js('resources/assets/js/vue.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');
My vue.js (located in the same directory as app.js and components.js) looks like this:
const app = new Vue({
el: '#app'
});
And my app.js:
require('./bootstrap');
window.Vue = require('vue');
Don't forget to tell mix to do its job:
npm run dev
Now where is the components.js script loaded? This happens in the newly created assets.blade.php (the 'defer' statement is important otherwise the script will be executed before the DOM is ready which will cause errors like 'unknown element #app'):
<script src="{{ asset('js/app.js') }}" defer></script>
@if(isset($components))
<script id="include-components" src="{{ asset('js/components.js') }}" data-components="{{ json_encode($components) }}" defer></script>
@endif
<script src="{{ asset('js/vue.js') }}" defer></script>
Here the script tag gets an option id="include-components"to access the element in the components.js to retrieve the data-components item which holds the json encoded list of key/value pairs of components.
Finally this blade file is included in the main app blade file (app.blade.php). Replace the reference to the app.js script by this:
<!-- Scripts -->
@include('layouts.assets')
Now it works perfectly fine for me and allows using .vue files for defining components. It reduces the clutter of having a lot of .js files and of having mix to tell there are a lot of .js files.
Thanks for the great work. I hope my modifications will help someone.