Javascript MVC with custom HTML elements - notifying controller - javascript

I have written custom HTML elements whose constructors and definitions themselves are in classes. I have done this in order to create a view. However, I need to create an app with a MVC, model-view-controller design pattern. I've seen several posts but all of them involve standard HTML elements. How can I do this with my custom HTML elements (tho not web-components; they don't use a shadow DOM)
ps: i'm not using Jquery but rather standard selectors.

You can use the MVC pattern for Custom Elements just like with standard Javascript.
Define:
one object (or a class) for the Controller
one for the Model
one for the View
There are many ways to define and to interconnect the Model, View and Controller.
Custom Element specific
Adapted to the Custom Element, you can either:
define the Custom Element class (extending HTMLElement) as the Controller
define the Custom Element class as the View
define the the Model, View and Controller inside the Custom Element class
define the 3 entities outside the Custom Element class
etc.
Example
In the minimalist example implemented below:
the Custom Element class defines the Controller
the Model is declared in the Controller as a function
then the View is declared in the Controller as a function, too
This way the View can access the Model (for data reading) and the Controller directly.
//CONTROLLER
class PersonController extends HTMLElement {
connectedCallback() {
let controller = this
let elem = this
//MODEL
function PersonModel () {
//Model API
this.load = async id => {
let res = await fetch( `https://reqres.in/api/users/${id}` )
let json = await res.json()
Object.assign( this, json.data )
controller.updateView()
}
}
let model = new PersonModel
//VIEW
function PersonView () {
elem.innerHTML = `
Id : <input id=index type=number>
<div id=card>Loading data
</div>`
let card = elem.querySelector( 'div#card' )
let index = elem.querySelector( 'input#index' )
index.onchange = () => controller.init( index.value)
//View API
this.update = () => {
card.innerHTML = `
<div>Name : ${model.first_name} ${model.last_name}</div>
<div><img src=${model.avatar}></div>`
index.value = model.id
}
}
let view = new PersonView
//Controller API
this.init = id => model.load( id )
this.updateView = () => view.update()
//Init with some data
this.init( 4 )
}
}
customElements.define( 'person-card', PersonController )
person-card { display: inline-block ; background-color: lightblue }
person-card input { width: 50 }
<person-card></person-card>
This example creates a <person-card> element with the MVC pattern that will fetch some data from a REST web service (at reqres.in) and show a person's name, id, and avatar.

Related

MVC Intersection Observer structure

I wonder if someone can assist me with MVC architecture. I took a course in MVC from Udemy and now I have a pet project I'm working on. In a nutshell. I have three JS files: controller, model and view.
I am watching activeHeading2 element and it a user scrolls past it, manipulates classes on two elements.
Anyways, What's happening now is when a user clicks and displays a new section with new activeHeading2 element, Observer still observes old section activeHeading2 in the view even if I tell it to unobserve or even disconnect. I am stuck on this for like a week now, any information or help would be beneficial.
I am not using any frameworks and this is vanilla JS in action:
// CONTROLLER:
constructor(model, view) {
this.view = view;
this.model = model;
// Init here:
this._cycleHeaderFooter();
this._refreshActiveElements();
this.view.bindToTop(this._handleToTop);
this.view.bindNavClick(this._handleNavClick);
this.view.bindObserver(this._handleObserver);
}
_handleNavClick = clicked => {
//View adjustment here
// Unobserve first before resetting ?
this.view.resetNavigation();
this.view.displaySection(clicked);
this._refreshActiveElements();
this.view.observe();
this.view.displayFooter();
this.view.activateNav(clicked);
}
const app = new Controller(new Model(), new View());
export default class View {
constructor() { }
bindObserver(fn){
// DOM, EVENTS,
fn(this.activeHeading2);
}
observe(activeHeading2){
const toggleObserver= (obs, img) =>{
console.log(obs);
if (obs === 'hide') {
this.main__topEl.classList.add('inactive');
this.headerEl.classList.remove('stickyHeader');
}
if (obs === 'show') {
this.main__topEl.classList.remove('inactive');
this.headerEl.classList.add('stickyHeader');
}
if (obs === 'img') {
// console.log(img.dataset.src);
img.src = img.dataset.src;
// Remove blur filter .lazy-img class
img.classList.remove('lazy-img');
}
}
const callback = function (entries, observer) {
const [entry] = entries;
if (entry.target === activeHeading2) {
entry.isIntersecting ? toggleObserver('hide') : toggleObserver('show');
}
}
const options = {
root: null,
threshold: 0,
}
let heading2Obs = new IntersectionObserver(callback, options);
heading2Obs.unobserve(this.activeHeading2);
heading2Obs.observe(this.activeHeading2);
}
}
Not sure why the view is stuck with old values ?
To run code on instantiation of a class, you need a method called contructor not construction.
Also in your code the view parameter passed into the constructor is not the same as this.view, you need to assign it. See code below as an example of what I mean.
class Controller {
constructor(model, view) {
this.view = view;
this.view.bindObserver(this._handleObserver);
}
_handleObserver = (activeHeading2) => {
this.view.observe(activeHeading2);
}
}
Trick to MVC Architecture is knowing how to import each JavaScript class so that it is accessible to other modules. Example:
class View {
}
export default new Class();
--> Then in controller: import view from './view.js'
this will allow controller to see all elements and methods inside that class.

Multiple controls in the aggregation binding using factory function

I want to include multiple controls in the items aggregation of the VBox control.
var title = new sap.m.Title({text: "Name"});
var nameInput = new sap.m.Input();
var nameText = new sap.m.Text();
var layout = new sap.m.VBox({
items: {
path: "/",
factory: function(sId, oContext) {
var type = oContext.getProperty("type");
if (type) {
return [title, nameInput];
} else {
return [title, nameText];
}
}
}
});
I want to add title and nameInput in the VBox when there is something in the type attribute and title and nameText when the type is empty or undefined. But it is returning an error:
Uncaught TypeError: o.setBindingContext is not a function
I am not sure why is this happening. It works when we return only single control in the factory function, but not the array. Does anyone has any clue how to return multiple controls in the aggregation binding using factory?
Factory function is supposed to return just one control instance, not an array. When I need multiple controls in one VBox item then I'd probably use a separate xml Fragment (e.g. another VBox or HBox) which in turn has many appropriate controls within.

How to define a variable accessible from all methods of the class?

I'm new to JavaScript and probably trying to emulate Ruby with this. I use StimulusJS, but I think the question is applicable to JS in general.
My goal is to run a method (on button click) which would fetch and display all subcategories of the category from the button. The method/function would first fetch the data and then manipulate the DOM. I wanted to split these into two methods, but when I call the other method (displaySubcategories) from within the first one (getSubcategories) the value of event changes. Same thing with this in the fetch block - I need to assign it first to self to be be later able to related to object itself.
Is there a better way to do this? Like variables accessible to all methods (instance variables in Ruby)? Or I shouldn't be splitting these into two functions at all (not JS way)?
import {Controller} from "stimulus"
export default class extends Controller {
static targets = ["categoryId", "categoryLabel", "subcategoryList", "subcategoryHeader"]
getSubcategories() {
let category_id = event.target.firstElementChild.value
let url = "/api/categories/" + category_id + "/subcategories?levels=1"
let self = this
let e = event
fetch(url)
.then(response => response.json())
.then(json_response => {
self.displaySubcategories(json_response, e)
})
}
displaySubcategories(json_response, event) {
subcategories.forEach(function (subcategory) {
let category_label_copy = this.cloneLabel(current_label, subcategory)
current_label.classList.add("chosen")
subcategory_list.appendChild(category_label_copy)
}, this)
}
}
expenses#getSubcategories">

How to get ViewRef values in Angular 2

So I'm trying to build a dynamic form in Angular2 (and NativeScript)..
What i've done so far is:
Get JSON from REST API
Add Component (via a template) to the ViewContainerRef
Now I want to "submit" the form. To do this, I thought I could simply use the ViewContainerRef and get its "siblings" (which are dynamically created based on the before mentioned JSON) and loop through them and create a JSON object to post this.. But I don't understand how the Angular2 DOM works..
Here is the (partial) code I've got so far:
addComponent function
//... class DynamicComponent, has id and value properties
public addComponent(container: ViewContainerRef, template: string){
#Component({template: template})
class TemplateComponent{
public value = "test";
}
#NgModule({declarations: [TemplateComponent]})
class TemplateModule {}
const mod = this.compiler.compileModuleAndAllComponentsSync(TemplateModule);
const factory = mod.componentFactories.filter((comp)=>
comp.componentType === TemplateComponent
);
const component = container.createComponent(factory[0]);
return TemplateComponent;
}
Create Components
#ViewChild("container",{ read: ViewContainerRef }) container : ViewContainerRef;
..
//foreach JSON element
let _component = <any>
this._dynamicComponent.addComponent(
this.container, elementObject.render()
);
//the render() returns things like : `<Label text="foo"></Label>`
..
_component.prototype.onTap = () => {
this.submit();
}
Submit Function
submit(){
let json=[];
for(let i=0;i<this.container.length;i++){
let component = this.container.get(i);
//Ideally I want to do this:
/*
json.push({
id:component.id,
val:component.value
});
*/
}
}
Basically, I get "component" in the submit function as a ViewRef object, but how do I continue from there?
Also, I'm fairly new to Angular2, so is this the right way? I've seen the form builder but I didn't get it to work properly with dynamically created elements..

Pass variables to AngularJS controller, best practice?

I am brand new to AngularJS and like what I've seen so far, especially the model / view binding. I'd like to make use of that to construct a simple "add to basket" piece of functionality.
This is my controller so far:
function BasketController($scope) {
$scope.products = [];
$scope.AddToBasket = function (Id, name, price, image) {
...
};
}
And this is my HTML:
<a ng-click="AddToBasket('237', 'Laptop', '499.95', '237.png')">Add to basket</a>
Now this works but I highly doubt this is the right way to create a new product object in my model. However this is where my total lack of AngularJS experience comes into play.
If this is not the way to do it, what is best practice?
You could use ng-init in an outer div:
<div ng-init="param='value';">
<div ng-controller="BasketController" >
<label>param: {{value}}</label>
</div>
</div>
The parameter will then be available in your controller's scope:
function BasketController($scope) {
console.log($scope.param);
}
You could create a basket service. And generally in JS you use objects instead of lots of parameters.
Here's an example: http://jsfiddle.net/2MbZY/
var app = angular.module('myApp', []);
app.factory('basket', function() {
var items = [];
var myBasketService = {};
myBasketService.addItem = function(item) {
items.push(item);
};
myBasketService.removeItem = function(item) {
var index = items.indexOf(item);
items.splice(index, 1);
};
myBasketService.items = function() {
return items;
};
return myBasketService;
});
function MyCtrl($scope, basket) {
$scope.newItem = {};
$scope.basket = basket;
}
I'm not very advanced in AngularJS, but my solution would be to use a simple JS class for you cart (in the sense of coffee script) that extend Array.
The beauty of AngularJS is that you can pass you "model" object with ng-click like shown below.
I don't understand the advantage of using a factory, as I find it less pretty that a CoffeeScript class.
My solution could be transformed in a Service, for reusable purpose. But otherwise I don't see any advantage of using tools like factory or service.
class Basket extends Array
constructor: ->
add: (item) ->
#push(item)
remove: (item) ->
index = #indexOf(item)
#.splice(index, 1)
contains: (item) ->
#indexOf(item) isnt -1
indexOf: (item) ->
indexOf = -1
#.forEach (stored_item, index) ->
if (item.id is stored_item.id)
indexOf = index
return indexOf
Then you initialize this in your controller and create a function for that action:
$scope.basket = new Basket()
$scope.addItemToBasket = (item) ->
$scope.basket.add(item)
Finally you set up a ng-click to an anchor, here you pass your object (retreived from the database as JSON object) to the function:
li ng-repeat="item in items"
a href="#" ng-click="addItemToBasket(item)"

Categories

Resources