Better way of applying css-modules to AngularJS - javascript

I'm using css-modules with AngularJS 1.5 components now. The stack is webpack + css-loader?modules=true + angular 1.5 components + pug.
Currently I have to do the following steps to use css modules in my pug template.
// my-component.js
import template from 'my-component.pug';
import styles from 'my-component.css';
class MyComponent {
constructor($element) {
$element.addClass('myComponent'); // ------ (1)
this.styles = styles; // ------ (2)
}
}
angular.module(name, deps)
.component('my-component', {
controller: MyComponent,
template: template,
});
// my-component.pug
div(class={{ ::$ctrl.styles.fooBar }}) FooBar // ----- (3)
// my-component.css
.myComponent { background: green; }
.fooBar { color: red; }
There are two problems:
Every component has to inject $element and set its class name manually. The reason for doing this is, AngularJS component tag itself exists in the result HTML without any classes, which makes CSS difficult. For example, if I use MyComponent above like this:
<div>
<my-component></my-component>
</div>
it will generate the following HTML:
<div>
<my-component>
<div class="my-component__fooBar__3B2xz">FooBar</div>
</my-component>
</div>
Compared to ReactJS, <my-component> in above result HTML is an extra, sometimes it makes CSS difficult to write.
So my solution is (1), to add a class to it.
The class in template is too long (3). I know it is the correct way to reference $ctrl.styles.fooBar but this is way too long.
My ideal solution would be like this:
// my-component.js
angular.module(name, deps)
.component('my-component', {
controller: MyComponent,
template: template,
styles: styles,
});
// my-component.css
div(css-class="fooBar") FooBar
The idea is to:
make angular.module().component support an extra styles attribute, which will automatically do (2) this.styles = styles; in the controller, and apply (1) $element.addClass() as well.
directive css-class to apply $ctrl.styles to element.
My question is, I have no idea how to implement idea 1 above (2 is easy). I appreciate if anyone could share some light on this.

I came up with a solution which I don't quite satisfy with.
Angular component can accept a function as template and inject with $element.
doc
If template is a function, then it is injected with the following locals:
$element - Current element
$attrs - Current attributes object for the element
Therefore we could attach the main class for component (.myComponent) in the template function, then regex replace all the occurance of class names with compiled class names.
// utils.js
function decorateTemplate(template, styles, className) {
return ['$element', $element => {
$element.addClass(styles[className]);
return template.replace(/\$\{(\w+)\}/g, (match, p1) => styles[p1]);
}];
}
// my-component.js
import style from './my-component.css';
import template from './my-component.pug';
import { decorateTemplate } from 'shared/utils';
class MyComponent {
// NO NEED to inject $element in constructor
// constructor($element) { ...
}
angular.module(name, deps)
.component('myComponent', {
// decorate the template with styles
template: decorateTemplate(template, styles, 'myComponent'),
});
// my-component.pug, with special '${className}' notation
div(class="${fooBar}") FooBar
There are still one place to be improved that decorateTemplate uses regex replacement and template has to use a special notation ${className} to specify the css-modules class names.
Any suggestions are welcome.
UPDATE
I updated my decorateTemplate() function to leverage pug features, so that local class names can be written as ._localClassName.
// utils.js
function decorateTemplate(template, styles, className) {
return ['$element', ($element) => {
$element.addClass(styles[className]);
return template.replace(/\sclass="(.+?)"/g, (match, p1) => {
let classes = p1.split(/\s+/);
classes = classes.map(className => {
if (className.startsWith('_')) {
let localClassName = className.slice(1);
if (styles[localClassName]) {
return styles[localClassName];
} else {
console.warn(`Warning: local class name ${className} not found`);
return className;
}
} else {
return className;
}
});
return ' class="' + classes.join(' ') + '"';
});
}];
}
// my-component.pug
._fooBar FooBar
Although this is much easier it does not eliminate the quirky notation (begin with _ for local class names) and regex replace.
Any suggestions are welcome.

Related

How to add class to Vue component via $refs

I need to add class name to some Vue components using their ref names. The ref names are defined in a config file. I would like to do it dynamically, to avoid adding class manually on each Vue component.
I have tried to find each component using $refs and if found, add the class name to the element's class list. The class is added, but it is removed as soon as user interaction begins (e.g. the component is clicked, receives new value etc.)
Here is some sample code I've tried:
beforeCreate() {
let requiredFields = config.requiredFields
this.$nextTick(() => {
requiredFields.forEach(field => {
if(this.$refs[field]) {
this.$refs[field].$el.classList.add('my-class')
}
})
})
}
You can use this:
this.$refs[field].$el.classList.value = this.$refs[field].$el.classList.value + 'my-class'
the only thing that you need to make sure of is that your config.requiredFields must include the ref name as a string and nothing more or less ... you can achieve that with :
//for each ref you have
for (let ref in this.$refs) {
config.requiredFields.push(ref)
}
// so config.requiredFields will look like this : ['one','two]
here is an example of a working sample :
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('one', {
template: '<p>component number one</p>'
})
Vue.component('two', {
template: '<p>component number two</p>'
})
new Vue({
el: "#app",
beforeCreate() {
let requiredFields = ['one','two'] // config.requiredFields should be like this
this.$nextTick(() => {
requiredFields.forEach(field => {
if(this.$refs[field]) {
this.$refs[field].$el.classList.add('my-class')
}
})
})
}
})
.my-class {
color : red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<one ref="one" ></one>
<two ref="two" ></two>
</div>
I know this question was posted ages ago, but I was playing around with something similar and came across a much easier way to add a class to $refs.
When we reference this.$refs['some-ref'].$el.classList it becomes a DOMTokenList which has a bunch of methods and properties you can access.
In this instance, to add a class it is as simple as
this.$refs['some-ref'].$el.classList.add('some-class')
You've to make sure classList.value is an array. By default its a string.
methods: {
onClick(ref) {
const activeClass = 'active-submenu'
if (!this.$refs[ref].classList.length) {
this.$refs[ref].classList.value = [activeClass]
} else {
this.$refs[ref].classList.value = ''
}
},
},
this post helped me tremendously. I needed to target an element within a v-for loop and I ended up writing a little method for it (i'm using Quasar/Vue).
hopefully this will save someone else some time.
addStyleToRef: function(referEl, indexp, classToAdd) {
//will add a class to a $ref element (even within a v-for loop)
//supply $ref name (referEl - txt), index within list (indexp - int) & css class name (classToAdd txt)
if ( this.$refs[referEl][indexp].$el.classList.value.includes(classToAdd) ){
console.log('class already added')
} else {
this.$refs[referEl][indexp].$el.classList.value = this.$refs[referEl][indexp].$el.classList.value + ' ' + classToAdd
}
}
let tag = this.$refs[ref-key][0];
$(tag).addClass('d-none');
Simply get the tag with ref let tag = this.$refs[ref-key][0]; then put this tag into jquery object $(tag).addClass('d-none'); class will be added to required tag.

How do I read a custom scss variable from javascript/typescript?

Say I need to share a variable (not a standard CSS value) between my SCSS and my Code (Typescript/JS)?
I need the variable in CSS for design, while in code for other aspects.
I thought about defining the variable in SCSS, and accessing it from Typescript (Angular2/Ionic2), but I can't understand how to do so. There are few examples in out there, but most are jQuery or just unfit for Ionic2/Angular2. I tried to define a class and add it to my ion-content, but I still cannot read my custom CSS properties/attributes(?) from it...
HTML:
...
<ion-content class="zozo">
...
TS:
#ViewChild( Content ) content: Content;
...
static getSassProperty( propName: string )
{
let ne: HTMLElement = MyApp.me.content.getElementRef().nativeElement;
let value = ne.style[ "--" + propName ];
return value;
}
SCSS:
.zozo
{
--cropper-width: $cropper-width;
--cropper-height: $cropper-height;
}

Knockout: get reference to component A to call one of A's function from component B?

Used Yeoman's Knockout generator (c.a. early 2015) which includes in require.js and router.js. Just using KO's loader.
Am attempting to call a function (ko.observable or not) in component "a" from component "b".
All the fluff below attempts to do merely:
// In componentB:
ComponentA.sayFoo();
Read KO docs on components and loaders, hacked for hours, etc. I don't want the overhead of say postal.js - and also could not get subscriptions (KO pub/sub) to work - I'm guessing for the same reason: the view models set up this way have no references to each other (?) - so the subscribers in one module don't see the publishers in another (right?) (… a bit over my head here %-)
1) Is this because the modules don't see each other… that this generated code does not place the KO stuff in a global namespace?
2) Trying to reach from one module to the other, seems to hinge on the getting the ref via the callback parms, using the function below, or is that incorrect? :
ko.components.get (name, callback) ;
startup.js using require looks like this:
define(['jquery', 'knockout', './router', 'bootstrap', 'knockout-projections'], function($, ko, router) {
// Components can be packaged as AMD modules, such as the following:
ko.components.register('component-a', { require: 'components/a/component-a' });
ko.components.register('component-b', { require: 'components/b/component-b' });
// [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.]
// [Scaffold component's N/A (I think?)]
// Start the application
ko.applyBindings({ route: router.currentRoute });
});
The (component) module A is straight forward, like this:
define(['knockout', 'text!./component-a'], function(ko, templateMarkup) {
function ComponentA (params) { console.log ('CompA'); } ;
ComponentA.prototype.sayFoo = function () { console.log ('FOO!'); } ;
ComponentA.prototype.dispose = function(){};
return { viewModel: ComponentA, template: templateMarkup };
});
Similarly, module B is:
define(['knockout', 'text!./component-b'], function(ko, templateMarkup) {
function ComponentB (params) { console.log ('Compb'); } ;
ComponentB.prototype.doFoo = function () {
//// B Needs to fire ComponentA.foo() … SEE CODE ATTEMPT BELOW
};
ComponentB.prototype.dispose = function(){};
return { viewModel: ComponentB, template: templateMarkup };
});
So this is where I'm stuck:
ComponentB.prototype.doFoo = function () {
ko.components.get ('component-a', ( function (parms) {
console.log ('parms.viewModel : ' + parms.viewModel );
// parms.viewModel is (unexpectedly) undefined ! So how to get the ref?
console.log ('parms.template : ' + parms.template );
// does have expected html objects, eg. [object HTMLwhatever], [object HTML...]
})) ;
This should be easy, or I'm dumbly leaving out something obvious!?
Maybe the modules need to be defined / set up differently?
Any suggestions would assist! Thx
This is just not how you'd normally communicate between knockout components.
Your options are:
1) Use https://github.com/rniemeyer/knockout-postbox. This is probably the best option as it integrates nicely with knockout. It is well documented and if you have troubles setting it up, you can always ask for help here.
2) Use any other global javascript EventBus (f.i. postal.js) and emit/subscribe to events in your components.
3) Have your root ViewModel pass common observables to each component as parameters - that way each component could modify/subscribe to the same observable.
4) (Probably what you want, although the worst scaling solution) If you give ids to the different components you could use ko.dataFor(document.getElementById("id")) to directly access the properties and methods of your components.
EDIT: In response to the comment:
I haven't
been able to determine what / where the root view model is:
ko.applyBindings({ route: router.currentRoute }) is the clue, but
router.js is convoluted. Suggestions on how to determine that?
Exactly - in your case the { route: router.currentRoute } object IS your root ViewModel. It currently only has one property called route, but you could definitely extend that.
For instance:
var rootViewModel = {
route: router.currentRoute,
mySharedObservable: ko.observable('hi!')
}
ko.applyBindings(rootViewModel);
Then you can pass that observable to multiple components as a parameter like this:
<div id="component-a" data-bind="component: { name: 'component-a', params: {router: $root.router, mySharedObservable: $root.mySharedObservable} }"></div>
<div id="component-b" data-bind="component: { name: 'component-b', params: {router: $root.router, mySharedObservable: $root.mySharedObservable} }"></div>
And finally you can use the new observable from within the component like this:
function ComponentB (params) {
this.mySharedObservable = params && params.mySharedObservable;
console.log(this.mySharedObservable());// This should log 'hi!'
};
You can now subscribe to the observable, change it and so on. It will be shared between components, so changing it one component will trigger the subscriptions in all components.
My standard approach would be to control the communication through the parent VM.
The parent VM can create a subscribable[1], and pass it to both componentA and componentB as a parameter; then ComponentA can subscribe to the subscribable, and ComponentB can trigger the subscribable.
//ComponentA
function ComponentA (params) {
var shouldSayFoo = params.shouldSayFoo;
this.shouldSayFooSubscription = shouldSayFoo.subscribe(function () {
this.sayFoo();
});
} ;
ComponentA.prototype.sayFoo = function () { console.log ('FOO!'); } ;
ComponentA.prototype.dispose = function () { this.shouldSayFooSubscription.dispose(); };
//In ComponentB
function ComponentB (params) {
this._triggerFoo = params.triggerFoo; //Same subscribable as shouldSayFoo in ComponentA
}
ComponentB.prototype.doFoo = function () {
this._triggerFoo.notifySubscribers(); //notifySubscribers triggers any .subscription callbacks
}
If ComponentA and ComponentB are siblings and you don't do this sort of stuff all the time, this works as a decently simple solution. If the components are "distant relatives" or if you find yourself doing this a lot, then I'd suggest some sort of pub-sub. And an advantage of this approach can be used by a lot of individual "A-B" pairs without interfering with each other, which is harder with a pub-sub system.
[1]: A ko.subscribable is an object with a subset of the observable functionality (ko.observable inherits from ko.subscribable): it doesn't let you read or write values, but lets you do .subscribe and .notifySubscribers. (They're created with new for some reason) You could use an observable, too, it's just a slight confusion of intent to create an observable if you don't intend it to hold a value.

How to dynamically set mixins for Reactjs Element

Is there any way to add mixins programmatically for an ReactElement?
For example
var mymixins = {
showLayout1(){
return "Layout1"
}
}
var comp = React.createClass({
onComponentWillMount(){
// dynamically add mixins "mymixins" to this element
addMixins(this, mymixins);
},
render(){
return this.showLayout1()
}
});
The reason I'm looking for this kind of function is because Reactjs Component does not accept 2 mixins with the same method name.

Is there a way to access a Vue Component's index — or some other unique identifier — from within its template?

If I'm creating, maybe dynamically, n number of components where each needs to refer to a unique index/id/reference from within its own template is there such a thing? I'm not using a for in loop to generate the components, so I don't think I have access to (read: already tried to use…) the $index.
I hacked together (poorly) a very bad illustration of what I'm trying to do, but I suspect there's a prebuilt method.
https://jsfiddle.net/itopizarro/dw070yyL/
Well I made a whole example before I read the part about you don't want to use $index so here's example if it helps: https://jsfiddle.net/dw070yyL/1/
But you CAN use v-ref to give a child component a unique identifier, see http://vuejs.org/api/#v-ref
<bar v-ref:bar-one></bar>
Then in the parent component you can use this.$refs.barOne to reference that child component.
This also can work with lists as seen in the docs:
<bar v-ref:bar-list v-for="item in list"></bar>
Would make this.$refs.barList an array of child components
I'll also say that the way you're doing it isn't too bad, for instance this Wizard component uses a similar method by iterating through this.$children in the ready() function and setting each of their index accordingly:
ready() {
this.countItems = this.$children.length
this.$children.forEach((item, index) => {
item.index = index
})
}
You can use a method to assign the index in the template without using a ready function.
Vue.component('task', {
methods: {
onStartGetIndex() {
this.$index = this.$root.$children.length;
return this.$index;
}
},
template: '<li>{{ onStartGetIndex() }}</li>'
});
Or save the index in an attribute to be referenced more in your template.
Vue.component('task', {
methods: {
onStartGetIndex() {
this.$index = this.$root.$children.length;
return this.$index;
}
},
template: '<li :index="onStartGetIndex()">{{ this.$index }}</li>'
});
The above two only work until a change is made and then the components refresh.
You can add a method that finds the index, and uses the length if it's not found.
Vue.component('task', {
methods: {
getIndex() {
if (this.$root.$children.indexOf(this) == -1) {
this.$index = this.$root.$children.length;
} else {
return this.$root.$children.indexOf(this);
}
},
},
template: '<li>{{ getIndex() }}</li>'
});

Categories

Resources