VueJS/Laravel - Sharing props between Laravel and Vue - javascript

I have defined a component called EditorNavigation.vue like so:
<template>
<div>
<ul>
<li v-bind:class="{'is-active':(active === field.id)}" v-for="field in fields">
<a :href="'/streams/' + stream_token + '/fields/' + field.id">{{field.name}}</a>
</li>
</ul>
<div>
</template>
<script>
export default {
props: ["fields", "active", "stream_token"],
created() {
this.fields = JSON.parse(this.fields);
this.active = JSON.parse(this.active);
this.stream_token = JSON.parse(this.stream_token);
}
};
</script>
As you can see in my component, I need three variables:
Fields (array of all fields)
An unique token for a specific resource
The current active field id (so I can set the is-active class).
In my Laravel view file, I use the component like this:
show.blade.php
<editor-navigation fields="{{ json_encode($stream->fields) }}" active="{{ json_encode($field->id) }}" stream_token="{{ json_encode($field->stream->token) }}"></editor-navigation>
So above code works fine, however it feels a bit "messy" - since I need to use the editor-navigation component in a lot of pages, and I am wondering what will happen as soon as I need another variable sent to it - I have to update it in all places.

Related

Can you use functions from an imported JavaScript library such as Change Case directly in a Vue component's template?

I understand how to import and use Change Case within the <script></script> element of a Vue component, which is just the standard Javascript import covered in the Change Case Github page. However, I would like to use the Change Case functions directly in the template if possible.
Currently, it is my understanding that for dynamic content in the template, in this case generated by v-for running through an array, I must render the return value of a intermediary method from the component's methods section which applies the Change Case function. A method is required for each case type (e.g. camelCase, snakeCase, etc.) I want to render, in this instance one (capitalCase). For example:
// ...
<div
v-for="location in locations"
:key="location.name"
>
<input
type="checkbox"
:id="`select-${location.name}`"
:value="capitalCaseLocationName(location.name)"
v-model="locationsInput"
/>
<label :for="`select-${location.name}`">
{{ capitalCaseLocationName(location.name) }}
</label>
</div>
// ...
methods: {
capitalCaseLocationName(name) {
return capitalCase(name)
}
},
// ...
It would be preferable to somehow import Change Case into the template logic so I could write it like this (no intermediary methods needed):
// ...
<div
v-for="location in locations"
:key="location.name"
>
<input
type="checkbox"
:id="`select-${location.name}`"
:value="capitalCase(location.name)"
v-model="locationsInput"
/>
<label :for="`select-${location.name}`">
{{ capitalCase(location.name) }}
</label>
</div>
// ...
Any chance of that being possible?
As long as you register the imported function as a method you should be able to use it directly in the template.
According to the code, you use Options API, so something like this should do the trick:
import {capitalCase} from "change-case";
...
methods: {
capitalCase,
myOtherMethod () => {...}
}
...
And in the <template>:
<input
type="checkbox"
:id="`select-${location.name}`"
:value="capitalCase(location.name)"
v-model="locationsInput"
/>
The functions need to be defined and passed to the template, that is why even console.log won't work from a template.
You already have an answer with an example, but here's another thing you could do that might make things easier.
You can create a helper like this:
template-helpers.js
export function capitalCase(str) {
return str.split(" ").map(wrd => wrd[0].toUpperCase() + wrd.slice(1)).join(" ")
}
export default {
capitalCase
}
this would make it so that you could use it in a composition/setup like this
import templateHelpers from "../utils/template-helpers.js";
setup(){
return{
...templateHelpers
}
}
in an options API component you could just include it like this
import templateHelpers from "../utils/template-helpers.js";
// ...
methods: {
...templateHelpers,
// other methods
}
// ...
Example
by exporting functions in export default you can destructure them by using methods: { ...templateHelpers
the downside is that it would all the methods every time, but it would make for a more convenient solution. Alternatively, you can pick and chose, since the functions are also exported
import {capitalCase} from "../utils/template-helpers.js";
// ...
methods: {
capitalCase,
// other methods
}
// ...
Vue does have a way to add global definitions, but it's discouraged. This would be done by assigning it to config.globalProperties
https://vuejs.org/api/application.html#app-config-globalproperties
app.config.globalProperties.capitalCase = (str) => {
return str.split(" ").map(wrd => wrd[0].toUpperCase() + wrd.slice(1)).join(" ")

How to provide a handlebars helper as a block param for another helper

I am stuck with Handlebars v4.1.2 and trying to employ a pattern I've seen in Ember and other component based frameworks, but I'm having trouble wrapping my mind around the context binding and options passing.
The point is to provide dropdownMenuItem for authors to fill out while not actually registering it as a helper that can be used outside of a dropdownMenu.
The usage I'd like is this:
{{#dropdown-menu label='Menu' as |dropdown-menu-item|}}
{{#dropdown-menu-item href='https://example.com'}}Example{{/dropdown-menu-item}}
<!-- assume that "links" is defined as Array.<{{url: string, text: string}}> -->
{{#each links as |link|}}
{{#dropdown-menu-item href=link.url}}{{ link.text }}{{/dropdown-menu-item}}
{{/each}}
{{/dropdown-menu}}
Here's what I've got so far:
const Handlebars = require("handlebars");
Handlebars.registerHelper('dropdown-menu', function dropdownMenu(options) {
return new Handlebars.SafeString(`
<menu class="dropdown">
<button class="dropdown__trigger">${options.hash.label}</button>
<div class="dropdown__content">
${options.fn(this, {
blockParams: [dropdownMenuItem]
})}
</div>
</div>
`);
})
function dropdownMenuItem(options) {
return `
<a href="${options.hash.url}" class="dropdown__link">
${options.fn(this)}
</a>
`;
}
The error is that the options object passed into the dropdownMenu function is not defined

ember-power-select Custom Search Action and 'selected' with external data

Overview
I'm using ember-power-select in a Ember.js 3.8 project - and it half works and half doesn't !
To make the question more readable I've put all the code at the bottom of the question.
Situation
The select is configured to fetch data from an API endpoint and provide the user with a set of possible options to select from.
The route (routes/guest/new-using-ember-power-select.js) involved does a createRecord of the model (models/guest.js) and then, ideally, changes made to both of the form elements (templates/guests/new-using-ember-power-select.js and templates/components/guest-form-ember-power-select.hbs) are reflected back into that record in the data store.
Issue
This works fine for the text input but I can't make it work for the ember-power-select.
In the current configuration (shown below) the user may :
search for options to select;
select an option and;
have that selection reflected back into the guest instance in the data store. However the choice made is not reflected in the user interface - there appears to have been no selection made.
I would really appreciate someone pointing out what I'm doing wrong here. I feel like it might be quite a small thing but it did occur to me that I have to manage the state of the select via properties in the component and only when the form is submitted update the underlying data-store .... I would prefer not to do that but I would be interested to know if that was thought to be the best idea.
Thanks
EDIT 1: I forgot to say that I have attempted to alter the onchange property of the ember-power-select so that instead of looking like this
onchange=(action "nationalityChangeAction")
... it looks like this ...
onchange=(action (mut item.nationality))
That has the effect that :
the value selected is visible in the form (as you would normally expect but unlike my current effort) but
the value placed into the underlying data store record is not a two character country code but instead an instance of the array returned the API call, an object with two properties {"name":"New Zealand","alpha2Code":"NZ"}.
Model
//app/models/guest.js
import DS from 'ember-data';
import { validator, buildValidations } from 'ember-cp-validations';
const Validations = buildValidations({
name: [
validator('presence', true),
],
nationality: [
validator('presence', true),
],
});
export default DS.Model.extend( Validations, {
name: DS.attr('string'),
nationality: DS.attr('string')
});
Route
//app/routes/guest/new-using-ember-power-select.js
import Route from '#ember/routing/route';
export default Route.extend({
model() {
return this.store.createRecord('guest', {
name: "",
nationality: ""
});
},
actions: {
updateNationality(slctnValue) {
this.controller.model.set('nationality' , slctnValue);
},
}
});
Template
//app/templates/guests/new-using-ember-power-select.js
<h2>Guest: Add New</h2>
<div class="well well-sm">
Demonstration of 'ember-power-select'
</div>
{{guest-form-ember-power-select
item=model
changeNationalityHandler="updateNationality"
updateRecordHandler="updateRecord"
cancelHandler="cancelAndExit"
}}
{{outlet}}
Component Template
//app/templates/components/guest-form-ember-power-select.hbs
<div class="form-vertical">
{{!-- Guest Name --}}
<div class="form-group">
<label class="control-label">Name</label>
<div class="">
{{ input type="text"
value=item.name
class="form-control"
placeholder="The name of the Guest"
focus-out=(action (mut this.errMsgDspCntrl.nameError) true)
}}
</div>
{{#if this.errMsgDspCntrl.nameError}}
<div class="text-danger">
{{v-get item 'name' 'message'}}
</div>
{{/if}}
</div>
<div class="form-group">
<label class="control-label">Countries (using power-select)</label>
<div class="">
{{#power-select
searchPlaceholder="Text to provide user info about what they can search on"
search=(action "searchCountries")
selected=item.nationality
onchange=(action (mut item.nationality))
as |countries|
}}
{{countries.name}}
{{/power-select}}
</div>
{{#if this.errMsgDspCntrl.nationalityError}}
<div class="text-danger">
{{v-get item 'nationality' 'message'}}
</div>
{{/if}}
</div>
{{!-- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--}}
{{!-- Buttons --}}
<div class="form-group">
<div class="">
<button type="submit" class="btn btn-default" {{action "buttonSaveClicked" item}}>{{buttonLabel}}</button>
<button type="button" class="btn btn-default" {{action "buttonCancelClicked" item}} >Cancel</button>
</div>
</div>
</div>
{{yield}}
Component
//app/components/guest-form-ember-power-select.js
import Component from '#ember/component';
export default Component.extend({
actions:{
searchCountries(term) {
//Response to :
//
//https://restcountries.eu/rest/v2/name/z?fields=name;alpha2Code
//
//
//looks like this
// [
// ...
// {"name":"New Zealand","alpha2Code":"NZ"}
// ...
// ]
//
let url = `https://restcountries.eu/rest/v2/name/${term}?fields=name;alpha2Code`
let dbg = fetch(url)
.then(function(response) {
return response.json();
});
return dbg;
},
nationalityChangeAction(slctn){
this.sendAction('changeNationalityHandler', slctn.alpha2Code);
},
}
});
I'm going to answer showing some diffs with the changes required to make the select work in your repo: https://github.com/shearichard/emberjs-select-addon-comparison
The key thing to understand is that ember-power-select receives a block, in your case
as |country|}}
{{country.name}}
{{/power-select}}
That block will be called to render each of the options, but also the selected option. In this case, the options are country objects with this shape: {"name":"American Samoa","alpha2Code":"AS"}. That is why you call {{country.name}} to render it. However, with your approach, the selected value that you are passing in is not an object with a name property. In fact is not even an object, but the string "AS" in the case of American Samoa, so you can output the name property of a string.
In your situation, the information you store (the country code) is not enough to display a nice "American Samoa" in the trigger of the select, and since you don't know the countries before hand until you make a search you can't look the country with that country code.
If you don't have an edit form, my suggestion is to store the entire selected country in a property which is the one you pass to selected.
diff --git a/app/components/guest-form-ember-power-select.js b/app/components/guest-form-ember-power-select.js
index edf9390..2467d85 100644
--- a/app/components/guest-form-ember-power-select.js
+++ b/app/components/guest-form-ember-power-select.js
## -25,6 +25,8 ## export default Component.extend({
//messages
nameOfErrMsgDspCntrl : 'errMsgDspCntrl',
+ nationality: undefined,
+
actions:{
searchCountries(term) {
## -73,7 +75,7 ## export default Component.extend({
},
nationalityChangeAction(slctn){
- //this.set(this.myValue, slctn);
+ this.set('nationality', slctn);
this.sendAction('changeNationalityHandler', slctn.alpha2Code);
},
diff --git a/app/templates/components/guest-form-ember-power-select.hbs b/app/templates/components/guest-form-ember-power-select.hbs
index 56f007d..5c69834 100644
--- a/app/templates/components/guest-form-ember-power-select.hbs
+++ b/app/templates/components/guest-form-ember-power-select.hbs
## -24,7 +24,7 ##
{{#power-select
searchPlaceholder="Text to provide user info about what they can search on"
search=(action "searchCountries")
- selected=item.nationality
+ selected=nationality
onchange=(action "nationalityChangeAction")
as |countries|
}}
## -36,14 +36,14 ##
This works as long as you don't want to edit the nationality of a user you created before, perhaps even weeks ago. You won't have a reference to the country in that case, only the country code. In that situation I'd recommend making selected a computed property that returns a promise the resolves to the country object with the user's country code, if your API allows that. And seems that it does, so the BEST solution would be
diff --git a/app/components/guest-form-ember-power-select.js b/app/components/guest-form-ember-power-select.js
index edf9390..f889734 100644
--- a/app/components/guest-form-ember-power-select.js
+++ b/app/components/guest-form-ember-power-select.js
## -1,4 +1,5 ##
import Component from '#ember/component';
+import { computed } from '#ember/object';
export default Component.extend({
buttonLabel: 'Save',
## -25,6 +26,16 ## export default Component.extend({
//messages
nameOfErrMsgDspCntrl : 'errMsgDspCntrl',
+ nationality: computed('item.nationality', function() {
+ let countryCode = this.get('item.nationality');
+ if (countryCode) {
+ return fetch(`https://restcountries.eu/rest/v2/alpha/${countryCode}?fields=name;alpha2Code`)
+ .then(function (response) {
+ return response.json();
+ });
+ }
+ }),
+
This last one will fetch the information for the country you know the code of.
selected property must be an element included in options provided to Ember Power Select. In your scenario you are not using options property but setting the options through search action but that doesn't make a big difference.
Your search action return an array of objects (e.g. [{"name":"New Zealand","alpha2Code":"NZ"}]). nationalityChangeAction sets the selected value to the value of alpha2Code. Therefore selected is not included in options:
[{"name":"New Zealand","alpha2Code":"NZ"}].includes('NZ') // false
So the state your Power Selects ends in is similar to this one:
<PowerSelect
#options={{array
(hash foo="bar")
}}
#selected="bar"
/>
A simplified version of what you are doing look like this:
<PowerSelect
#options={{array
(hash foo="bar")
}}
#selected={{selected}}
#onchange={{action (mut selected) value="foo"}}
/>
Please have a look in Ember Power Select documentation regarding the difference between using options and search:
When that's the case you can provide a search action instead of options (it's the only situation where the options are not mandatory) that will be invoked with the search term whenever the user types on the search box.
[...]
There is only three things to know about this action:
- You should return a collection or a promise that resolves to a collection from this action.
- You can provide both options and a search action. Those options will be the initial set of options, but as soon as the user performs a search, the results of that search will be displayed instead.
Therefore it doesn't make a difference for your issue if you are using options or returning a collection from search action. It all comes down to having a selected value that is not part of the collection bound to options or returned by search action.
This is actually the reason why your UI is working as expected if using onchange=(action (mut item.nationality)). In that case item.nationality is that to the selected object in collection returned by search (e.g. {"name":"New Zealand","alpha2Code":"NZ"}) and not to the value of it's alpha2Code property.
I'm using angle bracket component invocation syntax in my answer. Hope that fine. It makes it easier to read it in my opinion.

How to define a temporary variable in Vue.js template

Here is my current template:
<a-droppable v-for="n in curSize" :key="n - 1" :style="{width: `${99.99 / rowLenMap[orderList[n - 1]]}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLenMap[orderList[n - 1]] > 10}">
<some-inner-element>{{rowLenMap[orderList[n - 1]]}}</some-inner-element>
</a-draggable>
</a-droppable>
The problem is that i have to write rowLenMap[orderList[n - 1]] multiple times, and i'm afraid vue.js engine will also calculate it multiple times.
What i want is something like this:
<a-droppable v-for="n in curSize" :key="n - 1" v-define="rowLenMap[orderList[n - 1]] as rowLen" :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
I think it's not difficult to implement technically because it can be clumsily solved by using something like v-for="rowLen in [rowLenMap[orderList[n - 1]]]". So is there any concise and official solution?
I found a very simple (almost magical) way to achieve that,
All it does is define an inline (local) variable with the value you want to use multiple times:
<li v-for="id in users" :key="id" :set="user = getUser(id)">
<img :src="user.avatar" />
{{ user.name }}
{{ user.homepage }}
</li>
Note : set is not a special prop in Vuejs, it's just used as a placeholder for our variable definition.
Source: https://dev.to/pbastowski/comment/7fc9
CodePen: https://codepen.io/mmghv/pen/dBqGjM
Update : Based on comments from #vir us
This doesn't work with events, for example #click="showUser(user)" will not pass the correct user, rather it will always be the last evaluated user, that's because the user temp variable will get re-used and replaced on every circle of the loop.
So this solution is only perfect for template rendering because if component needs re-render, it will re-evaluate the variable again.
But if you really need to use it with events (although not advisable), you need to define an outer array to hold multiple variables at the same time :
<ul :set="tmpUsers = []">
<li v-for="(id, i) in users" :key="id" :set="tmpUsers[i] = getUser(id)" #click="showUser(tmpUsers[i])">
<img :src="tmpUsers[i].avatar" />
{{ tmpUsers[i].name }}
{{ tmpUsers[i].homepage }}
</li>
</ul>
https://codepen.io/mmghv/pen/zYvbPKv
credits : #vir us
Although it doesn't make sense here to basically duplicate the users array, this could be handy in other situations where you need to call expensive functions to get the data, but I would argue you're better off using computed property to build the array then.
Judging by your template, you're probably best off with a computed property, as suggested in the accepted answer.
However, since the question title is a bit broader (and comes up pretty high on Google for "variables in Vue templates"), I'll try to provide a more generic answer.
Especially if you don't need every item of an array transformed, a computed property can be kind of a waste. A child component may also be overkill, in particular if it's really small (which would make it 20% template, 20% logic and 60% props definition boilerplate).
A pretty straightforward approach I like to use is a small helper component (let's call it <Pass>):
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
Now we can write your component like this:
<Pass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" v-slot="{ rowLen }">
<a-droppable :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
</Pass>
<Pass> works by creating a scoped slot. Read more about scoped slots on the Vue.js documentation or about the approach above in the dev.to article I wrote on the topic.
Appendix: Vue 3
Vue 3 has a slightly different approach to slots. First, the <Pass> component source code needs to be adjusted like this:
const Pass = {
render() {
return this.$slots.default(this.$attrs)
}
}
Today I needed this and used <template> tag and v-for like this
I took this code and
<ul>
<li v-for="key in keys"
v-if="complexComputation(key) && complexComputation(key).isAuthorized">
{{complexComputation(key).name}}
</li>
</ul>
Changed it to this
<ul>
<template v-for="key in keys">
<li v-for="complexObject in [complexComputation(key)]"
v-if="complexObject && complexObject.isAuthorized">
{{complexObject.name}}
</li>
</template>
</ul>
And it worked and I was pleasantly surprised because I didn't know this was possible
This seems like the perfect use case of a child component. You can simply pass your complex computed value(s) as a property to the component.
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props
How about this:
<div id="app">
<div
v-for="( id, index, user=getUser(id) ) in users"
:key="id"
>
{{ user.name }}, {{ user.age }} years old
<span #click="show(user)">| Click to Show {{user.name}} |</span>
</div>
</div>
CodePen: https://codepen.io/Vladimir-Miloevi/pen/xxJZKKx
<template>
<div>
<div v-for="item in itemsList" :key="item.id">
{{ item.name }}
<input v-model="item.description" type="text" />
<button type="button" #click="exampleClick(item.id, item.description)">
Click
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{
id: 1,
name: 'Name1',
},
{
id: 2,
name: 'Name2',
},
],
}
},
computed: {
itemsList() {
return this.items.map((item) => {
return Object.assign(item, { description: '' })
})
},
},
methods: {
exampleClick(id, description) {
alert(JSON.stringify({ id, description }))
},
},
}
</script>
Just tested using vue3 and works, i think it works universally
{{ (somevariable = 'asdf', null) }}
<span v-if="somevariable=='asdf'">Yey</span>
<span v-else>Ney</span>
It outputs nothing while setting your variable.
mandatory:
opening "("
set your variable
closing ", null)"
curSize is an array. Your temporary values comprise a corresponding implied array sizedOrderList = curSize.map(n => orderList[n-1]). If you define that as a computed, your HTML becomes
<a-droppable v-for="n, index in sizedOrderList" :key="curSize[index]" :style="{width: `${99.99 / rowLenMap[n]}%`, order: n}">
<a-draggable :class="{thin: rowLenMap[n] > 10}">
<some-inner-element>{{rowLenMap[n]}}</some-inner-element>
</a-draggable>
</a-droppable>

Getting the value of element clicked on in Emberjs

I have a series of labels that I would like to retrieve the text from when the user clicks on them. I thought the jquery might be doable in Emberjs:
$(this).attr('text');
This however, doesn't work with Ember. My component view is this:
<ul class="list-inline">
{{#each model as |model|}}
<li {{action "sendToInput"}} class="label label-default">{{model.name}}</li>
{{/each}}
</ul>
I would like to retrieve that model.name value via the action "sendToInput".
In my component js file I have tried:
actions: {
sendToInput() {
$(this.target).attr('text');
}
}
I have also tried:
this.innerHTML;
$(this).text();
this.get('text');
$(this).val();
I have also opened up the console and dug through this and cannot seem to find where they store the element clicked on.
The documentation doesn't mention this and there's no issue I can find on the Github for ember-cli.
Thanks.
The common way is to pass a model name as an action parameter like this:
<ul class="list-inline">
{{#each model as |model|}}
<li {{action "sendToInput" model.name}} class="label label-default">{{model.name}}</li>
{{/each}}
</ul>
and then handle it in the action:
actions: {
sendToInput(name) {
// do something with name
}
}
But if you still need to access the exact element's html, there is another workaround:
export default Ember.Component.extend({
didInsertElement () {
this.$().on('click', '.label', function (event) {
$(this).text();
})
}
});

Categories