Dynamic Components
There are several cases where a Nexus package component needs to load a component defined in (a) another package, or (b) the application itself. For example:
- CMS: Block editor
- Users: Custom fields
- Auditing: Details renderer
The problem is, unlike Laravel Blade views, webpack needs to know where to find all possible components at build time, so it can bundle them into the JS files.
To solve this, we define specific directories for it to look in:
resources/<package>/vue/<component>/- Applicationvendor/alberon/<package>/assets/vue/<component>- This packagevendor/alberon/*/<package>/vue/<package>/<component>/- Other packages
For example, the Auditing package renderer would find components in:
resources/nexus-audits/Renderer/- Application-specific renderersvendor/alberon/nexus-audits/assets/vue/Renderer/- Default renderers provided by the Nexus Audits packagevendor/alberon/nexus-users/nexus-audits/vue/Renderer/- Additional renderers provided by the Nexus Users package- etc.
We use this structure so it's clear which package it is provided for, and which component it is used by. (It can technically be varied if needed, but please discuss with Dave first.)
How to Implement
There is a helper method dynamicComponents() in the base Nexus package. It works as follows:
<script>
// vendor/alberon/nexus-audits/assets/vue/Renderer.vue
import dynamicComponents from '@alberon/nexus/js/lib/dynamicComponents';
export default {
components: dynamicComponents([
// vendor/alberon/*/assets/<package>/vue/<component>/*.vue - Other packages
require.context('../../../', true, /^\.\/[^\/]+\/assets\/nexus-audits\/vue\/Renderer\/[^\/]+\.(vue|js|jsx|mjs|ts|tsx)$/),
// vendor/alberon/<package>/assets/vue/<component>/*.vue - This package
require.context('./Renderer', false, /\.(vue|js|jsx|mjs|ts|tsx)$/),
// resources/<package>/vue/<component>/*.vue - Application
require.context('@', true, /^\.\/nexus-audits\/vue\/Renderer\/[^\/]+\.(vue|js|jsx|mjs|ts|tsx)$/),
]),
props: {
name: { type: String, required: true },
},
}
</script>
<template>
<component
v-if="$options.components[name]"
:is="name"
/>
<div v-else class="text-danger">
Cannot load component "{{ name }}".
</div>
</template>
The syntax for loading dynamic components is a bit unwieldy - unfortunately we can't simplify it (e.g. call require.context() in the function) because Webpack has to be able to statically analyse it at build time.
The ../../../ path should point to the vendor/alberon/ folder - so update it if necessary. The regex then tells it to look in */assets/nexus-audits/vue/Renderer/ - update the package name and component name as needed.
The @ is an alias for the application resources/ folder, and should not be changed. It is last because later components take precedence over earlier ones, and we want the application to be able to override the packages. Note that we use a regex here too, rather than a hard-coded path like @/nexus-audits/vue/Renderer/ to stop webpack erroring if it doesn't exist.
The v-if and v-else block are there to give us more useful output if an invalid component name is given. (If you just use <component :is="name" />, you will get an error in the console instead and rendering will fail.) You can customise the error message if needed.
Any props will need to be added to the wrapper component and passed through to the dynamic components. Note that all dynamic components should accept the same props, or else they should have inheritAttrs: false set - otherwise the missing props will be interpreted by Vue as non-prop attributes and added to the HTML.
There is an example of this in the Nexus Demo package and application.
List of Available Components
For something like the audit logs, there is no need to get a list of components because they are defined in the PHP code (generally in enum classes - one per package + one for the application itself, if needed).
But for something like the CMS block editor, we need to show a list of available blocks to the user. For that, there is another helper method available - dynamicComponentsList():
<script>
import DynamicLoadingDemo from '@alberon/nexus-demo/vue/DynamicLoadingDemo';
import dynamicComponentsList from '@alberon/nexus/js/lib/dynamicComponentsList';
import {BDropdown, BDropdownItem} from 'bootstrap-vue';
export default {
components: {
BDropdown,
BDropdownItem
},
computed: {
components: () => dynamicComponentsList(DynamicLoadingDemo),
},
}
</script>
<template>
<BDropdown text="Add Block">
<BDropdownItem v-for="component in components" :key="component.name" @click="$emit('add', component.name)">
{{ component.label }}
</BDropdownItem>
</BDropdown>
</template>
First, implement the wrapper component (DynamicLoadingDemo in this example) as described in the section above. Make sure the only components it uses are dynamic ones.
Then import it and pass it to dynamicComponentsList(). You will get back an array of objects, each consisting of:
name- The component name, which should be used to load the dynamic componentlabel- The human-readable name (see below)component- The component itself, in case you need to do anything more advanced (e.g. organise components into categories or add icons)
To set the label for a dynamic component, define $_nexus_componentName on the component itself. For example:
<script>
export default {
$_nexus_componentName: 'Example Component',
props: {
//...
},
}
</script>
The $_nexus_componentName property is not reactive, so you may need to hard-refresh the page after updating it - Ctrl-click the "Reload Page" icon (top-right) to force the component state to be reloaded.
If $_nexus_componentName is not set, it will use the component name (filename without .vue) as a fallback label.
Note: The name $_nexus_componentName was chosen because it follows the Vue style guide. It can be customised by passing a second parameter to dynamicComponentsList(), but you should generally avoid that to ensure consistency. If you add additional fields, follow a similar structure, e.g. $_nexusCms_blockCategory.