I'm honestly not sure why this is not working. Seems to be a pretty standard operation. It is not mounting the component, is not throwing an error, and not running the function directly after it. All happing in cfg.AddToCart.vm.addToCart()
cfg.AddToCart = {
vm: {
init() {
return;
},
addToCart() {
let parent = document.getElementById('atc-error');
let errEl = document.getElementById('atc-error-component');
if(cfg.state.selections.SIZE) {
m.mount(errEl, null);
} else {
let component = new cfg.selectComponent(cfg.Options, cfg.optionsView);
m.mount(errEl, component);
cfg.util.toggleSlide(parent);
}
}
},
controller() {
cfg.AddToCart.vm.init();
}
};
cfg.AddToCart.view = function() {
return <div id="add-to-cart-container">
<div id="atc-error">
<span>Select a size and add to cart again.</span>
<div id="atc-error-component"></div>
</div>
<div class="small-12 columns">
<button class="large button alert"
onclick={() => {
this.vm.addToCart();
}}>
Add To Cart
</button>
</div>
</div>;
};
We use the new cfg.selectComponent(cfg.Options, cfg.optionsView) component multiple times throughout the application, so it is not an error with that. #atc-error is set to display:none, but that also doesn't seem to be the problem. This is not the only conditional mount in the application, so that is why I'm a bit stumped.
from looking at the way you've structured your code it strikes me you're missing out on a lot of Mithril's benefits. In particular:
If your 'vm' is indistinguishable from the controller, then you don't need to create and manage a whole separate object for that. Especially when you're using methods to control local component state, that is the job of the controller. The controller exposes an object to the view — this should be considered the 'vm' to that extent. Having a separate object to hold model state is useful when the state is relevant outside of the component instance: you already have this in your cfg.state, so in this scenario the vm is redundant.
Mithril views have a config method which exposes the real DOM element after every draw. You don't need to store references to view elements since you can do it here. This is a large part of what makes virtual DOM libraries so appealing: the view is clever, and you can introduce view-specific logic in them directly.
Components can be called directly from within the view, and the view can use conditional logic to determine whether or not to call them. m.mount is only necessary to initialise a Mithril application and define 'top level' components; from within Mithril code you can invoke nested components via m function directly.
A couple of other misunderstandings:
The controller executes before the view is rendered (and once it's executed, the properties it initialises are exposed to your view function as the first argument), so you can't access elements created by the view when the controller initialises.
The init function in the vm serves no purpose.
Here's a rewrite of your code that takes the above into account. I used plain Mithril instead of MSX to avoid compilation, but you could easily convert it back:
// Determine what your external dependencies are
const { state, selectComponent } = cfg
// Define the component
const AddToCart = {
// No need for a separate VM: it is identical in purpose & function to the controller
controller : function(){
// No need to store element references in the model: those are the view's concern.
// Keep the VM / ctrl size to a minimum by only using it to deal with state
this.addToCart = () => {
if( state.selections.SIZE )
this.showSize = false
else {
this.showSize = true
this.slideToErr = true
}
}
},
view : ctrl =>
m( '#add-to-cart-container',
m( '#atc-error', {
// Config exposes the element and runs after every draw.
config : el => {
// Observe state, and affect the view accordingly:
if( ctrl.slideToErr ){
el.scrollIntoView()
// Reset the state flag
ctrl.slideToErr = false
}
}
},
m( 'span', 'Select a size and add to cart again.' ),
// This is an and condition, ie 'if A, then B
ctrl.showSize
// This is how you invoke a component from within a view
&& m( selectComponent )
),
m( '.small-12 columns',
m( 'button.large button alert', {
onclick : () =>
ctrl.addToCart();
},
'Add To Cart'
)
)
)
}
Worked by changing it to this pattern:
cfg.AddToCart = {
vm: {
init() {
this.errorComponent = m.prop();
},
addToCart() {
let parent = document.getElementById('atc-error');
let errEl = document.getElementById('atc-error-component');
if(cfg.state.selections.SIZE) {
cfg.util.toggleSlide(parent);
setTimeout(() => {
this.errorComponent(null);
}, 400);
} else {
let component = new cfg.selectComponent(cfg.Options, cfg.optionsView);
this.errorComponent(component);
setTimeout(() => {
cfg.util.toggleSlide(parent);
}, 100);
}
}
},
controller() {
cfg.AddToCart.vm.init();
}
};
cfg.AddToCart.view = function() {
return <div id="add-to-cart-container">
<div id="atc-error">
<span>Select a size and add to cart again.</span>
<div id="atc-error-component" class="row">
{this.vm.errorComponent() ? m.component(this.vm.errorComponent()) : ''}
</div>
</div>
<div class="small-12 columns">
<button class="large button alert"
onclick={() => {
this.vm.addToCart();
}}>
Add To Cart
</button>
</div>
</div>;
};
Related
I want to implement the autocomplete search (the one on the left) from this codepen to pure web components. But something went wrong because slots don't work and something else also doesn't work but I can't figure out what it is. What I have so far
Search-select
const template = `
<p>
<slot name="autocomp" results="${this.results}"
searchList="${(event) => this.setQuery(event)}"
>
fgfgfg
</slot>
yo
</p>
`;
class SearchSelect extends HTMLElement {
constructor() {
super();
this.query = "";
this.results = [];
this.options = [
"Inside Out",
"John Wick",
"Jurassic World",
"The Lord of the Rings",
"Pacific Rim",
"Pirates of the Caribbean",
"Planet of the Apes",
"Saw",
"Sicario",
"Zombies",
];
this.shadow = this.attachShadow({ mode: "open" });
}
setQuery(event) {
console.log(event.target);
this.query = event.target.value;
}
get options() {
return this.getAttribute("options");
}
set options(val) {
this.setAttribute("options", val);
}
static get observedAttributes() {
return ["options", "filterMethod"];
}
filterMethod(options, query) {
return options.filter((option) =>
option.toLowerCase().includes(query.toLowerCase())
);
}
attributeChangedCallback(prop, oldValue, newValue) {
if (prop === "options") {
this.results = this.filterMethod(this.options, this.query);
this.render();
}
if (prop === "filterMethod") {
this.results = this.filterMethod(this.options, this.query);
this.render();
}
}
render() {
this.shadow.innerHTML = template;
}
connectedCallback() {
this.render();
}
}
customElements.define("search-select", SearchSelect);
Autocomplete
const templ = `
<search-select>
<div class="autocomplete">
<input
type="text"
placeholder="Type to search list"
onchange="${this.searchList}"
onfocus="${this.showDropdown}"
onblur="${this.hideDropdown}"
/>
<div class="autocomplete-dropdown" v-if="dropdownVisible">
<ul class="autocomplete-search-results-list">
${this.result}
</ul>
</div>
</div>
</search-select>
`;
class Autocomplete extends HTMLElement {
constructor() {
super();
this.dropdownVisible = false;
this.rslts = "";
this.shadow = this.attachShadow({ mode: "open" });
}
get results() {
return this.getAttribute("results");
}
set results(val) {
this.setAttribute("results", val);
}
get searchList() {
return this.getAttribute("searchList");
}
showDropdown() {
this.dropdownVisible = true;
}
hideDropdown() {
this.dropdownVisible = false;
}
attributeChangedCallback(prop, oldValue, newValue) {
this.render();
}
render() {
this.shadow.innerHTML = templ;
}
connectedCallback() {
this.render();
}
}
customElements.define("auto-complete", Autocomplete);
Your current approach is completely wrong. Vue is reactive framework. Web components do not provide reactivity out of box.
The translation of Vue2 component to direct Web component is not straight forward. The slots do not work because Vue.js slots are not the same as Web component slots. They are just conceptually modeled after them.
First, when you use the Vue.js slot, you are practically putting some part of the vDOM (produced as a result of JSX) defined by the calling component into the Search or Autocomplete component. It is not a real DOM. Web components, on the other hand, provide slot which actually accepts a real DOM (light DOM).
Next, your render method is practically useless. You are simply doing this.shadow.innerHTML = template; which will simply append the string as HTML into the real DOM. You are not resolving the template nodes. Vue.js provides a reactivity out of box (that's why you need Vue/React). Web components do not provide such reactivity. On each render, you are re-creating entire DOM which is not a good way to do it. When you are not using any framework to build web component, you should construct all the required DOM in connectedCallback and then keep on selectively updating using DOM manipulation API. This is imperative approach to building UIs.
Third, you are using named slot while consuming it in auto complete, you are not specifying the named slot. So whatever is the HTML you see is not getting attached to the Shadow DOM.
You will need to
Building a complex component like Auto Complete needs a basic reactivity system in place that takes care of efficiently and automatically updating the DOM. If you do not need full framework, consider using Stencil, LitElement, etc. If you can use Vue.js, just use it and wrap it into Web component using helper function.
For Vue 2, you can use the wrapper helper library. For Vue 3, you can use the built-in helper.
When using m.route, does everything under it have to be rendered using Hyperscript (not counting JSX)? Is it possible to mix pure HTML with multiple separate parts rendered using Hyperscript?
Suppose I have this HTML code:
<div id="root">
<div class="header">
<h3 id="header-text">Header Text</h3>
</div>
<div class="container">
<div id="content"></div>
<div id="sidebar"></div>
</div>
<div id="footer"></div>
</div>
Is it possible to use Mithril to render multiple parts of the HTML sections? Like so (pseudo code):
var root = document.getElementById('root');
var header = document.getElementById('header');
var content = document.getElementById('content');
var sidebar = document.getElementById('sidebar');
var footer = document.getElementById('footer');
var HeaderComponent = ...; // to render header text
var ContentComponent = ...; // main page content
var SidebarComponent = ...; // show links, bio, etc.
var Footer = ...; // contact info, etc.
var index = {
view: function(){
return [
{el: header, component: HeaderComponent},
{el: content, component: ContentComponent},
{el: sidebar, component: SidebarComponent},
{el: footer, component: FooterComponent}
];
}
};
m.route(root, '/', {
'/': index
});
so that multiple separate parts in the HTML code are rendered instead of just having a single root element? I really don't want to render the whole HTML skeleton template in Mithril as well, like this:
❌ DO NOT WANT:
// I DON'T want this
m('div#root', [
m('div.header', [
m('h3#header-text', m(HeaderComponent))
]),
m('div.container', [
m('div#content', m(ContentComponent)),
m('div#sidebar', m(SidebarComponent))
]),
m('div#footer', m(FooterComponent))
]);
I know it's manageable, but I'd really like the base skeleton template to be inside the .HTML file, so that I can seamlessly wrap more HTML tags later on, like using Bootstrap classes (cards, container, rows, extra wrapping divs needed, etc.). Thanks :)!
There's nothing stopping you from using the m.mount function several times, and several mount-points can be initialised this way:
var count = 0
m.mount(root1, {
view: () =>
m('button', {
onclick: () => count++,
textContent: 'Increase count',
}),
})
m.mount(root2, {
view: () =>
m('p', 'Count: ', count)
})
<h1>
Static page with multiple roots
</h1>
<div id=root1>
</div>
<p>
Intermediary static content
</p>
<div id=root2>
</div>
<script src="https://unpkg.com/mithril#2.0.4/mithril.min.js"></script>
Using #Barney's answer below, I managed to solve this using a mix of m.route for the main content and multiple m.mount's for the other components:
var header = document.getElementById('header');
var content = document.getElementById('content');
var sidebar = document.getElementById('sidebar');
var footer = document.getElementById('footer');
var HeaderComponent = ...; // to render header text
var SidebarComponent = ...; // show links, bio, etc.
var Footer = ...; // contact info, etc.
// this will be shown under content
var HomePage = {
view: function(){
return m('p', [
'Lorem ipusom dolor amit ',
m(m.route.Link, {href: '/about'}, 'About')
]);
}
};
var AboutPage = {
view: function(){
return m('p', 'This is the About page!');
}
};
//// MAIN SOLUTION ////
var Layout = {
// oninit is only run once
oninit: function(){
m.mount(header, HeaderComponent);
m.mount(sidebar, SidebarComponent);
m.mount(footer, FooterComponent);
},
// run on every route change:
view: function(vnode){
return m('div', vnode.children);
}
};
m.route(content, '/', {
// using RouteResolver
'/': {
render: function(){
return m(Layout, m(HomePage));
},
onmatch: function(){
// this will be called on route change, so update your mounted components as needed
HeaderComponent.title = "Home Page";
}
},
'/about': {
render: function(){
return m(Layout, m(AboutPage));
},
onmatch: function(){
HeaderComponent.title = "About Page";
}
}
});
m.route is only used for <div id="content">, while all the other separate components (header, sidebar, footer) are instantiated using m.mount. I've wrapped the router with a Layout component that's used for all the routes, as this allows me to init and mount the separate components that will remain when routes change and not be affected by the main router. The router had to be modified to use RouteResolver to allow for more control and flexbility.
Notes:
Only a single m.route allowed per application, but multiple m.mount's allowed
When the same component is assigned multiple routes (in this case, Layout), the subtree will NOT be deleted and rebuilt from ground up -- it will only be diffed and updated (source). This way, we can use Layout component's oninit lifecycle hook to mount our other separate components, because oninit will only be called once.
We must use a RouteResolver for the routes in m.route, which gives us more control over how the routes should be rendered and we also get hooks like onmatch, which fires BEFORE the route is initialized -- this is the perfect place to update our mounted components as needed!
How can i declare a computed property using Nuxt ? or the equivalent ?
I am using NuxtJs and trying to use a category filter.
I want to filter by unique categories, and i am getting this error message:
Cannot read property 'filter' of undefined
I trying to adapt to Nuxtjs the exemple i found in this pen : https://codepen.io/blakewatson/pen/xEXApK
I declare this computed property below, first at pages/index.vue and after into .nuxt/App.js
filteredStore: function() {
var vm = this;
var category = vm.selectedCategory;
if(category=== "All") {
return vm.stores;
} else {
return vm.stores.filter(function(stores) {
return stores.category === category;
});
}
}
And i try to apply the filter into this list of checkboxes :
<div class="columns is-multiline is-mobile">
<div class="column is-one-quarter" v-for="store in filteredStore" :key="store.id" :store="store">
<label class="checkbox">
<input type="checkbox" v-model="selectedCategory" :value="''+store.category">
{{store.category}}
</label>
</div>
</div>
I'm going to do some guessing at your code situation (based on the example you noted), so just let me know where I make an incorrect assumption. I would guess that something like the following could work for you... maybe you could provide additional details where I'm missing them.
With regards to your error Cannot read property 'filter' of undefined, that probably means your array of stores is undefined. I believe if you create the stores array as empty in the data section, you should at least have it available before your async call returns any results.
One possible thing to you can do to test if your filtering logic is working... is to uncomment the manually created data array that I've created below. It's like an inline test for your data structure and logic, removing the asynchronous retrieval of your data. This basically can check if the filter works without your API call. It would narrow down your issue at least.
export default {
data() {
return {
stores: [
// Let's assume you don't have any static stores to start on page load
// I've commented out what I'm guessing a possible data structure is
//
// Example possible stores in pre-created array
// { name: 'Zales', category: 'Jewelry', id: 1 },
// { name: 'Petco', category: 'Pet Shop', id: 2 },
// { name: 'Trip Advisor', category: 'Tourism', id: 3 },
// { name: 'Old Navy', category: 'Clothes', id: 4 }
],
selectedCategory: 'All'
}
},
computed: {
// Going to make some small js tweaks
filteredStores: () {
const vm = this;
const category = vm.selectedCategory;
if (category === "All") {
return vm.stores;
} else {
return vm.stores.filter(store => {
return store.category === category;
});
}
}
},
async asyncData({ $axios }) {
$axios
.$get('https://yourdomain.com/api/stores/some-criteria')
.then(response => {
this.stores = response.data;
})
.catch(err => {
// eslint-disable-next-line no-console
console.error('ERROR', err);
});
}
};
And then your HTML
<div class="columns is-multiline is-mobile">
<div class="column is-one-quarter" v-for="store in filteredStores" :key="store.id" :store="store">
<label class="checkbox">
<input type="checkbox" v-model="selectedCategory" :value="`${store.category || ''}`">
{{store.category}}
</label>
</div>
</div>
ANYWAY This is all just a big guess and what your scenario is, but I figured I'd try to help shape your question some so that you could get a more meaningful response. In general, I'd suggest trying to provide as much detail as you can about your question so that people really can see the bits and pieces where things might have gone astray.
Don't touch anything in .nuxt Someone noted that above in a comment, and it's very important. Essentially that whole directory is generated and any changes you make in it can be easily overwritten.
I am new to Vue.js and am trying to create components that will simplify form creation, based on a library I have been using for a while now (PHP).
I have created a component that renders a label + textbox, styled via Bootstrap.
In order to avoid having to pass all the parameters every time, I want to be able to define defaults from within the parent, so that they will stay in effect until changed.
The component looks like this (MyTextBox.vue)
<template>
<div v-bind:class="myDivWidth">
<label v-bind:class="`control-label ${myLabelWidth}`">{{label}}</label>
<div v-bind:class="myControlWidth">
<input class="form-control col-md-12" v-bind:value="value">
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
// trying to use this as 'class' variable but most likely wrong
myDefaultLabelWidth: 4
}
},
props: {
label: String,
labelWidth: String,
controlWidth: String,
divWidth: String,
value: {required: false},
defaultLabelWidth: {type: String}
},
computed: {
myLabelWidth: function () {
let lw;
//debugger;
do {
if (typeof this.defaultLabelWidth !== 'undefined') {
lw = this.defaultLabelWidth;
// ****** Note the call to the parent function
this.$parent.setDefault('defaultLabelWidth', lw);
break;
}
if (typeof this.labelWidth !== 'undefined') {
lw = this.labelWidth;
break;
}
if (typeof this.lw !== 'undefined') {
lw = this.lw;
break;
}
// ****** Note the call to the parent function
lw = this.$parent.getDefault('defaultLabelWidth');
} while (false);
return `col-md-${lw}`;
},
// snip....
}
}
</script>
and it is used like this (I am only showing attributes relating to label, for brevity)
(StoryEditor.vue)
<my-textbox label="LableText1" default-label-width=4></my-textbox>
<my-textbox label="LableText2"></my-textbox>
<my-textbox label="LableText3" label-width=5></my-textbox>
<my-textbox label="LableText4"></my-textbox>
<my-textbox label="LableText5" default-label-width=6></my-textbox>
<my-textbox label="LableText6"></my-textbox>
<my-textbox label="LableText7"></my-textbox>
What this is meant to do, is set the label with to 4, for the first 2 instances
then force a width of 5 for the next instance
then go back to 4
then set a new default of 6 for the remaining 3 components.
This is useful in cases where a lot of components (of the same type) are used, most of which are of the same width.
This mechanism will also used for all other applicable attributes.
Please note that what is important here is that the default is set in the parent and can change between instances of the component.
(I am aware that I can have a default value in the template itself but, as I understand it, that would apply to all instances of that component)
Any help would be greatly appreciated!
[Edit]
I have found one solution:
I added these methods to the parent (StoryEditor.vue).
They are called by the component code, shown above with '******' in the comments
<script>
export default {
created: function () {
// make sure the variable exists
if (typeof window.defaultOptions === 'undefined') {
window.defaultOptions = {
defaultLabelWidth: 3,
defaultControlWidth: 7
};
}
},
data() {
return {
story: {
}
}
},
methods: {
getDefaultOptions: () => {
console.log('getDefaultOptions', window.defaultOptions);
},
setDefaultOptions: (opts) => {
window.defaultOptions = opts;
},
getDefault: (option) => {
console.log(' getDefault', window.defaultOptions);
return window.defaultOptions[option];
},
setDefault: (option, v) => {
window.defaultOptions[option] = v;
console.log('setDefault', window.defaultOptions);
}
}
}
</script>
This uses this.$parent. to call methods in the parent.
The parent then uses a window variable to store/retrieve the relevant parameters.
A window variable is used because I want to have a single variable that will be used by all instances of the component.
In my angularjs app I am rendering my "navigation-bar" div for every page.
After user logs in and redirect to details page, then I want I am updating $scope which is not reflected into the view.
To reflect the change of $scope I am calling $digest by using $scope.$apply(). Seems it not updating and the $scope update still not reflecting in my view.
My code looks like below:
CONTROLLER:
function NavCtrl($scope){
//as isAuth false
$scope.showLogin = true;
$scope.showLogout = false;
}
function ProductDetails($scope){
//as isAuth true
$scope.showLogin = false;
$scope.showLogout = true;
if (!$scope.$$phase) {
//$digest or $apply to reflect update of scope update
$scope.$apply();
}
}
VIEW:
<div id="navigation-bar" ng-controller="NavCtrl">
<li ng-show="showLogin">Login</li>
<li ng-show="showLogout">Logout</li>
</div>
What I am doing wrong? Am I missing any point? By the way I went through other questions like AngularJS $scope updates not reflected in the view but it doesn't help in my case.
The main problem is that you are using two different controllers. Each controller has it's own scope (i.e. the scope is not global), so when you change showLogin in your ProductDetails it is only the local scope that changes, not that of NavCtrl.
You can either rewrite it to use one controller, or pass data between your controllers, there are several ways to do that:
You can set data on the $rootScope . The rootScope is a global scope that all controllers can access and that you can always use in templates. I recommend keeping the rootScope to a minimum since the rootScope is shared everywhere and you might end up with a big mess of variables in it.
You can pass data through a service. Only one instance of each service is ever created, so different controllers can access the same service and get the same data.
You can use .$broadcast to send a signal to any child controller. This is probably also best kept to a minimum, a lot of broadcasts will sooner or later slow you down (although the limit should be high).
The problem is you should be using a single controller for this. Here is an example of a controller that will display the correct menu depending on his connected scope variable. The value is set with AJAX or somewhere else in your code.
CONTROLLER:
function NavCtrl($scope, $rootScope){
$scope.showLogin = true;
$scope.showLogout = false;
$rootScope.$watch("connected", function() {
if ($rootScope.connected) {
$scope.showLogin = false;
$scope.showLogin = true;
} else {
$scope.showLogin = true;
$scope.showLogin = false;
}
});
}
VIEW:
<div id="navigation-bar" ng-controller="NavCtrl">
<li ng-show="showLogin">Login</li>
<li ng-show="showLogout">Logout</li>
</div>
Connect me
I suggest an AuthUserService to keep track of login state, and injecting that service into your NavCtrl and any other controllers that need to know about the user:
.factory('AuthUserService', ['$http','$q',
function($http, $q) {
var user = {};
return {
user: function() {
return user;
},
login: function(user_credentials) {
$http.post('/login', user_credentials).then(
function(response) {
angular.copy(response.data, user);
...
});
},
logout: function() { ...
}
}
}])
.controller('NavCtrl', ['AuthUserService','$scope',
function(AuthUserService, $scope) {
$scope.user = AuthUserService.user();
$scope.$watch('user.name', function(name) {
if(name) {
$scope.loggedIn = true;
} else {
$scope.loggedIn = false;
}
});
}])
<div ng-controller="NavCtrl">
<li ng-hide="loggedIn">Login</li>
<li ng-show="loggedIn">Logout</li>
</div>