While approaching Xamarin forms cross platform development, I'm struggling with the definitions of reusable controls.
As a first and very basic example, I've developed a dummy component which looks like this:
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vmBase="clr-namespace:TestApp.ViewModels.Base"
mc:Ignorable="d"
vmBase:ViewModelLocator.AutoWireViewModel="True"
x:Class="TestApp.Views.Templates.HashtagContainerTemplateView"
x:Name="this">
<ContentView.Content>
<StackLayout BindingContext="{Reference this}">
<Label Text="{Binding Test}"/>
</StackLayout>
</ContentView.Content>
</ContentView>
Where the code behind is:
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TestApp.Views.Templates
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class HashtagContainerTemplateView : ContentView
{
public static readonly BindableProperty TestProperty = BindableProperty.Create(
propertyName: nameof(Test),
returnType: typeof(string),
declaringType: typeof(HashtagContainerTemplateView),
defaultBindingMode: BindingMode.TwoWay,
defaultValue: "I am the default value",
propertyChanged: TestPropertyChanged);
private static void TestPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
System.Diagnostics.Debugger.Break(); // This is called only when binding with constant values
}
public string Test
{
get => (string)GetValue(TestProperty);
set
{
SetValue(TestProperty, value);
System.Diagnostics.Debugger.Break(); // This is never called
}
}
public HashtagContainerTemplateView()
{
InitializeComponent();
}
}
}
I am trying to load this view into a page by binding the Test property with a value set by the parent ViewModel as you can see here:
namespace TestApp.ViewModels
{
public class MainViewModel : BaseViewModel
{
private string _testString;
public string TestString
{
get => _testString;
set
{
_testString = value;
RaisePropertyChanged();
}
}
public MainViewModel()
{
}
// This is called by the View Model Locator
public override async Task InitializeAsync(object navigationData)
{
TestString = "I am the binded string";
await base.InitializeAsync(navigationData);
}
}
}
Finally, the View is loaded into the parent Page as this:
<templates:HashtagContainerTemplateView Test="I am a costant string"/><!--This Works-->
<templates:HashtagContainerTemplateView Test="{Binding TestString}"/> <!--Not Working-->
When running the App the labels displayed are:
I am a costant string which is as expected
I am the default value which is the default value for the Property, instead of the one I've passed through binding
After some debugging I realized that the TestPropertyChanged is called only when binding with constant values and the Test setter is never called - see the breakpoints inside the code behind, right above - so I think this is the point...
I know there are many topics like this, even here on SO, but I really can't make it work... I believe there is something really simple I'm missing ...
Final note: I am using the Microsoft eShopOnContainers project as a reference, hence I am using the View Model Locator approach. This is why the intialization is not in the ctor but in the InitializeAsync function.
Microsoft itself has a section about Content Views in the documentation, but no bindings are used...
You need to change the following:-
Text="{Binding Test, Source={x:Reference this}}"
And remove the PropertyChanged to null.
I use your code and it works well on my side. I guess you forget to set the BindingContext in the Parent Page.
Here is how I use it:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _testString;
public string TestString
{
get => _testString;
set
{
_testString = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("TestString"));
}
}
}
public MainViewModel()
{
TestString = "I am the binded string";
}
}
And in Xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:templates="clr-namespace:App593"
mc:Ignorable="d"
x:Class="App593.MainPage">
<StackLayout>
<!-- Place new controls here -->
<templates:View1 Test="I am a costant string"/>
<templates:View1 Test="{Binding TestString}"/>
</StackLayout>
</ContentPage>
Remember to add BindingContext = new MainViewModel(); in the Parent Page. Other codes in my project is exactly the same as yours.
The problem was the View Model Locator, which was messing with the binding context and was just a leftover from a copy/paste.
Removing the
vmBase:ViewModelLocator.AutoWireViewModel="True"
from the Xaml solved the issue!
Thanks to #ValeriyKovalenko who made me spot it!
Related
I am practicing vue and I am trying to build a pagination with Rick Morty Api https://rickandmortyapi.com/documentation/
Currently looks like:
I would like to display these buttons in this form 1 2 3 4 5 ... 20 if I click on 20, then it would look like 1 ... 15 16 17 18 19 20. How can I achieve this? Do I need to use css for this, or pure js and use computed property?
<div class="button_container">
<button #click="pageChange(i + 1)" v-for="(item, i) in pages" :key="i">
{{ i + 1 }}
</button>
</div>
it's not that straight forward to do it in a one-liner, the approach I would recommend is to use a computed
you could use the function from https://stackoverflow.com/a/67658442/197546
// define method
function paginate(current_page, last_page, onSides = 3) {
// pages
let pages = [];
// Loop through
for (let i = 1; i <= last_page; i++) {
// Define offset
let offset = (i == 1 || last_page) ? onSides + 1 : onSides;
// If added
if (i == 1 || (current_page - offset <= i && current_page + offset >= i) ||
i == current_page || i == last_page) {
pages.push(i);
} else if (i == current_page - (offset + 1) || i == current_page + (offset + 1)) {
pages.push('...');
}
}
return pages;
}
// then in component...
data:{
return{
pages:[...],
currentPage: 0,
}
},
//...
computed: {
pageNums = () {
return paginate(this.currentPage, this.pages.length, 4)
}
}
then, because the ... should not have an event listener, you can use <template> and v-if to use different element
<div class="button_container">
<template v-for="(pageNum, i) in pages" :key="i">
<button v-if="Number.isInteger(pageNum)" #click="currentPage = i">
{{ pageNum }}
</button>
<span v-else>
{{ pageNum }}
</span>
</template >
</div>
I'm trying to build a website using Wordpress as my backend, WPGraphQL & Apollo-Vue, and Vue for my frontend. It's going ok so far, but Apollo is causing me a lot of grief. I opted to go for Vue3 since it seems that's the future for the library, and I want to learn the latest not the obsolete, and make use of the new features and improvements in 3.
I'm hearing that a lot of libraries and packages are still incompatible and error prone with Vue 3... Some forum posts are saying things about apollo-vue incompatibilities with Vue3, but I'm also new to the Vue system and it's entirely possible I could just be coding it wrong as well.. I'm not sure... Here is my code.. please tell me what I'm doing wrong so I can stop pulling my hair out..
browser console error
runtime-core.esm-bundler.js?5c40:217 Uncaught TypeError: Object(...) is not a function
at setup (HeaderPrimary.vue?da5e:26)
at callWithErrorHandling (runtime-core.esm-bundler.js?5c40:154)
at setupStatefulComponent (runtime-core.esm-bundler.js?5c40:6542)
at setupComponent (runtime-core.esm-bundler.js?5c40:6503)
at mountComponent (runtime-core.esm-bundler.js?5c40:4206)
at processComponent (runtime-core.esm-bundler.js?5c40:4182)
at patch (runtime-core.esm-bundler.js?5c40:3791)
at mountChildren (runtime-core.esm-bundler.js?5c40:3975)
at mountElement (runtime-core.esm-bundler.js?5c40:3896)
at processElement (runtime-core.esm-bundler.js?5c40:3868)
setup # HeaderPrimary.vue?da5e:26
callWithErrorHandling # runtime-core.esm-bundler.js?5c40:154
setupStatefulComponent # runtime-core.esm-bundler.js?5c40:6542
setupComponent # runtime-core.esm-bundler.js?5c40:6503
mountComponent # runtime-core.esm-bundler.js?5c40:4206
processComponent # runtime-core.esm-bundler.js?5c40:4182
patch # runtime-core.esm-bundler.js?5c40:3791
mountChildren # runtime-core.esm-bundler.js?5c40:3975
mountElement # runtime-core.esm-bundler.js?5c40:3896
processElement # runtime-core.esm-bundler.js?5c40:3868
patch # runtime-core.esm-bundler.js?5c40:3788
componentEffect # runtime-core.esm-bundler.js?5c40:4298
reactiveEffect # reactivity.esm-bundler.js?a1e9:42
effect # reactivity.esm-bundler.js?a1e9:17
setupRenderEffect # runtime-core.esm-bundler.js?5c40:4263
mountComponent # runtime-core.esm-bundler.js?5c40:4222
processComponent # runtime-core.esm-bundler.js?5c40:4182
patch # runtime-core.esm-bundler.js?5c40:3791
render # runtime-core.esm-bundler.js?5c40:4883
mount # runtime-core.esm-bundler.js?5c40:3077
app.mount # runtime-dom.esm-bundler.js?830f:1259
eval # main.js?56d7:41
./src/main.js # app.js:1557
__webpack_require__ # app.js:849
fn # app.js:151
1 # app.js:1582
__webpack_require__ # app.js:849
checkDeferredModules # app.js:46
(anonymous) # app.js:925
(anonymous) # app.js:928
main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import VueCompositionAPI from 'vue'
// import { DefaultApolloClient } from 'vue'
import { ApolloClient, createHttpLink, InMemoryCache } from 'vue'
// HTTP connection to the API
const httpLink = createHttpLink({
// You should use an absolute URL here
uri: 'http://localhost/websitename/graphql',
})
// Cache implementation
const cache = new InMemoryCache()
// Create the apollo client
const apolloClient = new ApolloClient({
link: httpLink,
cache,
})
createApp(App)
.use(router)
.use(VueCompositionAPI)
.use(apolloClient)
.mount('#app')
HeaderPrimary
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import VueCompositionAPI from 'vue'
import { DefaultApolloClient } from 'vue'
createApp(App)
.use(router)
.use(VueCompositionAPI) //DO I need to do this? How do I access it in a component.. I'm not clear on how this works..
.use(DefaultApolloClient) // same question..
.mount('#app')
<template>
<header>
<nav id="nav_primary">
<Loading_Spinner v-if="loading" />
<ul>
<li v-for="item in menu_items" :key="item">
<router-link :to="{ path: '/'+item.ID } ">{{ item.title }}</router-link>
</li>
</ul>
</nav>
</header>
</template>
<script>
import Loading_Spinner from './Loading_Spinner.vue'
import { watch, useQuery, gql } from 'vue'
//const { ref, reactive } = VueCompositionAPI //I don't know what this is? I found it somewhere...
export default {
name: 'HeaderPrimary',
setup () {
const {result} = useQuery(gql`
query getMenu {
posts {
edges {
node {
id
title
}
}
}
}
`)
watch(() => {
console.log(result.value)
})
},
data() {
return {
loading: false,
menu_items: [],
}
},
components: {
Loading_Spinner
}
}
package.json
{
"name": "websitename",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --dest ./",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"#apollo/client": "^3.3.19",
"#vue/apollo-composable": "^4.0.0-alpha.12",
"apollo": "^2.29.0-alpha.0",
"core-js": "^3.6.5",
"graphql": "^15.5.0",
"graphql-tag": "^2.12.4",
"vue": "^3.0.0",
"vue-apollo": "^3.0.0-alpha.3",
"vue-ionicons": "^3.0.5",
"vue-router": "^4.0.8"
},
"devDependencies": {
"#vue/cli-plugin-babel": "~4.5.0",
"#vue/cli-plugin-eslint": "~4.5.0",
"#vue/cli-service": "~4.5.0",
"#vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0",
"node-sass": "^6.0.0",
"sass": "^1.32.13",
"sass-loader": "^10.2.0",
"vue-cli-plugin-apollo": "~0.22.2",
"vue-loader": "^16.2.0",
"vue-template-compiler": "^2.6.12",
"webpack": "^4.42.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
I have a view V_BaseData_Extract in MS SQL server with one column Comments having null values as well having other textual values too. Column type is nvarcahr.
What's happening is that SQL returning same count for both below mentioned queries.
First Query:
select count(*)
from V_BaseData_Extract where Comments
not in ( ' ' , '
')
Second Query:
select count(1) from V_BaseData_Extract a where a.Comments is not null
Its handling null same as the string mentioned in my first query's where condition. What could be the reason behind that ? Am I missing something ?
Is null equivalent to some number of blank spaces ?
This is the correct explanation. Comments above helped me look into right direction and search relevant terms.
NULL values inside NOT IN clause
I have Custom component using my own directive (v-color):
<custom v-color="color" />
And my script, which I define this.color and this.changeColor():
{
data () {
return {
color: red
}
},
methods: {
changeColor (color) {
this.color = color
}
}
}
How can I write the code of v-color directive to change v-bind:color of <custom />?
In other words, the value of v-bind:color will be red when the component is loaded. If this.color is modified by a method (such as this.changeColor('blue')), value of v-bind:color would be auto-updated.
I would appreciate solutions that avoid "watch", because I will use v-color many times.
Something like this seems to fit what you're looking for:
Vue.component('third-party-component', {
props: ['color'],
template: '<div :style="{ color }" v-cloak>{{color}}</div>'
});
Vue.component('hoc-component', {
props: ['color'],
computed: {
transformedColor () {
if (this.color === "blu") return "blue";
if (this.color === "re") return "red";
if (this.color == "or") return "orange";
if (this.color == "pur") return "purple";
return this.color;
}
},
template: '<third-party-component :color="transformedColor" />'
});
new Vue({
el: '#app'
});
<html>
<body>
<div id="app" v-cloak>
<div>
<hoc-component color="blu"></hoc-component>
<hoc-component color="or"></hoc-component>
<hoc-component color="re"></hoc-component>
<hoc-component color="pur"></hoc-component>
<hoc-component color="pink"></hoc-component>
<hoc-component color="green"></hoc-component>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
</body>
</html>
Here we are taking advantage of the Higher Order Component pattern in order to modify the data we need and pass it on to the third party component. This is a much more effective way of mutating and handling data change without the side effects that directives have.
Hope this helps!
I found this code on codepen and made some changes in it. I am trying to filter the results after clicking the search button. But the point is that now it's filtering instantly when you type in the search box.
Here is the code:
new Vue({
el: '#app',
data: {
selected: [2],
search: '',
items: [{
action: '15 min',
headline: 'Brunch this weekend?',
title: 'Ali Connors',
subtitle: "I'll be in your neighborhood doing errands this weekend. Do you want to hang out?"
},
{
action: '2 hr',
headline: 'Summer BBQ',
title: 'me, Scrott, Jennifer',
subtitle: "Wish I could come, but I'm out of town this weekend."
},
{
action: '6 hr',
headline: 'Oui oui',
title: 'Sandra Adams',
subtitle: 'Do you have Paris recommendations? Have you ever been?'
},
{
action: '12 hr',
headline: 'Birthday gift',
title: 'Trevor Hansen',
subtitle: 'Have any ideas about what we should get Heidi for her birthday?'
},
{
action: '18hr',
headline: 'Recipe to try',
title: 'Britta Holt',
subtitle: 'We should eat this: Grate, Squash, Corn, and tomatillo Tacos.'
}
]
},
computed: {
filteredItems() {
return _.orderBy(this.items.filter(item => {
if(!this.search) return this.items;
return (item.title.toLowerCase().includes(this.search.toLowerCase()) ||
item.action.toLowerCase().includes(this.search.toLowerCase()) ||
item.headline.toLowerCase().includes(this.search.toLowerCase()) ||
item.subtitle.toLowerCase().includes(this.search.toLowerCase()));
}), 'headline');
}
},
methods: {
clearSearch () {
this.search="";
},
toggle(index) {
const i = this.selected.indexOf(index)
if (i > -1) {
this.selected.splice(i, 1)
} else {
this.selected.push(index)
}
}
}
})
I will share complete code in the comment where you can see a complete working example. How can this search filter only after clicking the search button?
The reason it's filtering instantly when you type in the search box is because filteredItems is a computed property which means it's gonna run every time the value of search changes which is every time you type a new character.
To filter the items only after the button is clicked, remove filteredItems from computed and create a filterItems function under your methods and attach that handler to the button's click event.
methods: {
filterItems() {
this.filteredItems = _.orderBy(
this.items.filter(item => {
if (!this.search) return this.items;
return (
item.title.toLowerCase().includes(this.search.toLowerCase()) ||
item.action.toLowerCase().includes(this.search.toLowerCase()) ||
item.headline.toLowerCase().includes(this.search.toLowerCase()) ||
item.subtitle.toLowerCase().includes(this.search.toLowerCase())
);
}),
"headline"
);
}
}
<button type="button" class="btn btn-lg btn-danger" #click="filterItems">Search</button>
Notice that I assigned the result of the function to filteredItems
which is a new property that you should add to your data object.
The reason is that your filterItems function should not mutate the original items but create a new array when it executes otherwise it would cause a bug if you mutate the original items and try filtering it again.
So in your data object, add filteredItems which will have the initial value equal to the items since it's not filtered yet when your app is mounted.
const items = [];
new Vue({
data: {
filteredItems: items,
items: items
}
})
See working implementation.
Note that I also call filterItems() when you clear your search so that it resets the data but you can just remove it from clearSearch() if you don't want that behaviour.