While working with React, i would like to display component name in an attribute of the component. E.g. if I have a component <LoginBox /> I would like it to be rendered as
<div data-react-name="LoginBox">...</div>
But I want this to be done automatically for each transpiled component. Reason for this is automated testing when I'd check for rendered elements in HTML/DOM, currently a component is not differentiated by the name in rendered HTML.
I thought I'd write a babel plugin, but I have no idea what visitors I'd use and how to make it robust enough. I tried google for such a plugin but I have no idea how it would be called and found nothing useful.
So is there any plugin or any way to achieve this?
Thanks
Now after a year, as I'm rethinking, it should be quite easy.
For more details on writing plugins see handbook.
Use ASTexplorer to inspect what AST would your code result in. And then, for generated tree, prepare visitors. So e.g. with code:
<div><Custom some-prop="prop">Some text</Custom></div>
we would infer, that we need to use visitor JSXOpeningElement and alter node's property attribute. To this property - array we would add a new element that we would create by Babel.types.jsxAttribute(name, value). We will get the name of tag from node's property .name.name (the name string is nested inside name object). We also need to use appropriate types. So it would look like this:
module.exports = function(Babel) {
return {
visitor: {
JSXOpeningElement(path) {
const name = Babel.types.jsxIdentifier('data-testname');
const value = Babel.types.stringLiteral(path.node.name.name);
path.node.attributes.push(Babel.types.jsxAttribute(name, value));
}
}
};
};
The code is tested with the ASTExplorer.
Related
So basically I am playing with Svelte trying to spin up a quick app, details of the app aren't important but basically it hosts a bunch of embedded sites. see example here & for replicability:
https://svelte.dev/repl/6f3484554ef8489b9a5960487a0a1f95?version=3.47.0
My problem is that when I add a new url & title to the sites list, the {#each} block that creates the embedded views doesn't update to reflect the new state of the list, even though the list is clearly updating in the console output. Is it something to do with scope or is it a Svelte issue of not triggering reactivity on prop reassignments from components?
Update: some sites don't allow embedding so use https://wikipedia.org as a safe one for testing.
if you replace a hard-coded url in the sites list with wiki address it should work fine. i basically want a new window to pop up as the {#each} block creates a new SiteView component
There are several things wrong with your code, the first being that you do not propagate the changes made to the sites array back to the main application, you should use bind: to keep the two arrays in sync.
<InputBar bind:sites {site} />
The second is that you are modifying an object when adding a new site and then adding that object to the array, this will always be the same object so if you change it the previously added sites will also change. You can solve this by spreading the new object into the array instead:
function add() { sites = sites.concat({...site}); console.log(sites)}
// or alternatively
function add() { sites = [...sites, {...site}]; console.log(sites); }
That said, the application is not very "Svelte" like as it mixes responsibilities and exposes data to components that don't need that data. For example, why would the input bar need to know about the current sites ? It would be a lot better to have the input bar be just that, an input bar. When the user clicks 'add' it raises an event that says 'something has been added' and resets the fields. Then the parent is responsible to add it to the array. This will make for a more flexible solution. If you do that you will see there is also no reason to have a 'site' variable on the top level (or even have that object at all, you can just have two fields)
<script>
import { createEventDispatcher } from 'svelte'
let url = ''
let title = ''
const dispatch = createEventDispatcher()
function add() {
dispatch('add', { url, title })
url = ''
title = ''
}
</script>
<div class="rounded">
<p>Enter a site to stream:</p>
<input type="text" placeholder="www.example.com" bind:value={url}>
<br>
<input type="text" placeholder="example" bind:value={title}>
<button on:click={add}>add</button>
</div>
<InputBar on:add={(ev) => sites = [...sites, ev.detail]} />
On a final note, to add things to the head of the html use <svelte:head> instead.
If you want to change a value from another component you need to bind the property, otherwise the relationship is one-way only (from parent component to child).
<InputBar bind:sites {site}/>
I just started using Sapper (https://sapper.svelte.technology) for the first time. I really like it so far. One of the things I need it to do is show a list of the components available in my application and show information about them. Ideally have a way to change the way the component looks based on dynamic bindings on the page.
I have a few questions about using the framework.
First, I'll provide a snippet of my code, and then a screenshot:
[slug].html
-----------
<:Head>
<title>{{info.title}}</title>
</:Head>
<Layout page="{{slug}}">
<h1>{{info.title}}</h1>
<div class="content">
<TopBar :organization_name />
<br>
<h3>Attributes</h3>
{{#each Object.keys(info.attributes) as attribute}}
<p>{{info.attributes[attribute].description}} <input type="text" on:keyup="updateComponent(this.value)" value="Org Name" /></p>
{{/each}}
</div>
</Layout>
<script>
import Layout from '../_components/components/Layout.html';
import TopBar from '../../_components/header/TopBar.html';
let COMPONENTS = require('../_config/components.json');
export default {
components: {
Layout, TopBar
},
methods: {
updateComponent(value) {
this.set({organization_name: value});
}
},
data() {
return {
organization_name: 'Org Name'
}
},
preload({ params, query }) {
params['info'] = COMPONENTS.components[params.slug];
return params;
}
};
</script>
Now my questions:
I notice I can't #each through my object. I have to loop through its keys. Would be nice if I could do something like this:
{{#each info.attributes as attribute }}
{{attribute.description}}
{{/each}}
Before Sapper, I would use Angular-translate module that could do translations on strings based on a given JSON file. Does anyone know if a Sapper/Svelte equivalent exists, or is that something I might need to come up with on my own?
I'm not used to doing imports. I'm more use to dependency injection in Angular which looks a bit cleaner (no paths). Is there some way I can create a COMPONENTS constant that could be used throughout my files, or will I need to import a JSON file in every occurence that I need access to its data?
As a follow-up to #3, I wonder if there is a way to better include files instead of having to rely on using ../.. to navigate through my folder structure? If I were to change the path of one of my files, my Terminal will complain and give errors which is nice, but still, I wonder if there is a better way to import my files.
I know there has got to be a better way to implement what I implemented in my example. Basically, you see an input box beside an attribute, and if I make changes there, I am calling an updateComponent function which then does a this.set() in the current scope to override the binding. This works, but I was wondering if there was some way to avoid the function. I figured it's possible that you can bind the value of the input and have it automatically update my <TopBar> component binding... maybe?
The preload method gives me access to params. What I want to know if there is some way for me to get access to params.slug without the preload function.
What would be really cool is to have some expert rewrite what I've done in the best possible way, possibly addressing some of my questions.
Svelte will only iterate over array-like objects, because it's not possible to guarantee consistent behaviour with objects — it throws up various edge cases that are best solved at an app level. You can do this sort of thing, just using standard JavaScript idioms:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} ...</p>
{{/each}}
<!-- or, if you need the key as well -->
{{#each Object.entries(info.attributes) as [key, value]}}
<p>{{attr.description}} ...</p>
{{/each}}
Not aware of a direct angular-translate equivalent, but a straightforward i18n solution is to fetch some JSON in preload:
preload({ params, query }) {
return fetch(`/i18n/${locale}.json`)
.then(r => r.json())
.then(dict => {
return { dict };
});
}
Then, you can reference things like {{dict["hello"]}} in your template. A more sophisticated solution would only load the strings necessary for the current page, and would cache everything etc, but the basic idea is the same.
I guess you could do this:
// app/client.js (assuming Sapper >= 0.7)
import COMPONENTS from './config/components.json';
window.COMPONENTS = COMPONENTS;
// app/server.js
import COMPONENTS from './config/components.json';
global.COMPONENTS = COMPONENTS;
Importing isn't that bad though! It's good for a module's dependencies to be explicit.
You can use the resolve.modules field in your webpack configs: https://webpack.js.org/configuration/resolve/#resolve-modules
This would be a good place to use two-way binding:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} <input bind:value=organization_name /></p>
{{/each}}
Yep, the params object is always available in your pages (not nested components, unless you pass the prop down, but all your top-level components like routes/whatever/[slug].html) — so you can reference it in templates as {{params.slug}}, or inside lifecycle hooks and methods as this.get('params').slug, whether or not a given component uses preload.
I am working on angular 4 project, I had a requirement to drag item, So I was using ng2-dragula plugin to do it for me. Now I need to extract data-id attribute from each row after drop to specific position.
dragulaService.drop.subscribe(
(args) =>{
const [bagName, elSource, bagTarget, bagSource, elTarget] = args;
console.log('after', $(bagSource)); // element after the inserted element
});
In $bagSource variable I will get each rows in new order. Say it be
<tr attributes data-id="23"><tr>
<tr attributes data-id="2"><tr>
<tr attributes data-id="32"><tr>
How could I extract data-id from each tr fields. I need to get each id in their new order in an array.
My required result should be [23,2,32];
jquery $(element).attr("data-id") equivalent in angular 4.
Something similiar to jquery $(element).attr("data-id") can be achieved with #ViewChild and some old-fashioned javascript. It would look like:
Use this only when drag zone and table is not related (if it is use #user3263307 answer it's more Angular-friendly). However the best option would be make use of [attr.data-id]="someVariable", but since I don't know your app structure can't help you with implementing this method.
.*html File
<table #tableRef>
<tr attributes [attr.data-id]="23"><tr>
<tr attributes [attr.data-id]="2"><tr>
<tr attributes [attr.data-id]="32"><tr>
</table>
Simply #tableRef is something like template variable which angular will bind with #ViewChild in component file.
*.component.ts
#ViewChild() tableRef: ElementRef;
public getIds() {
if (!this.tableRef|| !this.tableRef.nativeElement)
return;
//assuming that table would have only one `tbody` tag.
let tableBody = this.tableRef.tBodies[0];
if (!tableBody)
return;
// we need to use Array.from, because `.children` returns HTMLCollection<Element>
// type which is not handy to iterate through it.
let childs: Array<HTMLTableSectionElement> = Array.from(tableBody.nativeElement.children);
let arrIds = [];
for(let child of childs) {
if (!(child.nodeName && child.nodeName.toLowerCase() == "tr"))
continue;
arrIds.push(child.dataset.id);
}
}
Please note that it's a bit brutal method for angular since it's best not to interfere into HTML DOM structure. However as long we are only getting data from elements everything should be fine.
Please note that too about using #ViewChild.
Use this API as the last resort when direct access to DOM is needed. Use templating and data-binding provided by Angular instead. Alternatively you can take a look at Renderer2 which provides API that can safely be used even when direct access to native elements is not supported.
Relying on direct DOM access creates tight coupling between your application and rendering layers which will make it impossible to separate the two and deploy your application into a web worker.
From Angular docs link
In angular attributes should be defined like this:-
<tr attributes [attr.data-id]="23"><tr>
And attribute can be accessed using dataset property of Eventtarget object:-
dragulaService.drop.subscribe(
(args) =>{
const [bagName, elSource, bagTarget, bagSource, elTarget] = args;
console.log('id is:', elTarget.dataset.id);
});
I am creating a vue webapp, I have few pages with Dynamic content and also few pages which has mostly static content. I want to move all these static strings to one place.
One option can be to use vue-i18n or vue-multilanguage, these gives support to have content files like this, but I really have no use case of support of multiple languages, so it also seems a bit over kill to me.
Another option can be to have a vuex store for all the strings, vuex I am already using for state management.
What can be good approach to do this.
I am not aware of a standard way of doing this, also this would be applicable to all the web frameworks. That said it is an interesting and valid problem.
If I had to do something about it:
I would want these strings to be available everywhere.
I would prefer not having to import these strings in all the components and each time I needed to use them.
I would want the storage space to be descriptive so that I don't have to go back and forth to check what I want to import. [The toughest part in my opinion]
To achieve 1, we can use:
Vuex
A services/some.js file which exports an object.
Plugins
I would go with plugins because:
I can get the strings by merely using this in a component, Vue.use(plugin) prevents the same plugin getting used twice, and at the same time achieve all the points (3rd will still be a tough nut to crack). Only disadvantage that I know of it might clutter the vue-instance.
So plugin can be designed like:
// stringsHelperPlugin.js
const STRING_CONST = {
[component_1_Name]: {
key1: val1,
key2: val2,
....
},
[component_2_Name]: {
key1: val1,
key2: val2,
....
},
...
}
StringConst.install = function (Vue, options) {
Vue.prototype.$getStringFor = (componentName, key) => {
return STRING_CONST['componentName'][key]
}
}
export default StringConst
in main.js this can be used like:
import StringConst from 'path/to/plugin'
Vue.use(StringConst)
and you could use this in a component template like so:
<div>
{{ $getStringFor(<component_1_name>, 'key1') }}
</div>
You can use something like this.$getStringFor(<componentName>, key) in a method. Pretty much everything that vuejs to has to offer.
Why I call the 3rd point hardest is: Maintainance if you ever change component names, you might also have to change it in the object returned by the plugin. This problem again, can be handled in many ways.
You can make an npm module with JSON files containing your strings
If you don't use vuex in your project, put your content in some javascript files which will be basically objects with all your static content and import them where you need just like Belmin menionted I am using Vue js and python flask as my backend. I want to have some local variable set. How can it be done?
A similar approach can be used for urls, configurations, errors etc.
If you use vuex, centralize everything there and make getters which you can use in each of your components.
I have started working on Knockout recently, and have been assigned to create custom components that can be used in various application. While creating a component, I used camel case to name it, e.g: "datePicker".
Component code
ko.components.register("datePicker",{
viewModel: require('./components/date-picker-widget'),
template: require('raw!./components/date-picker-widget.html')
});
HTML Code
<datePicker params="{value:returnValue, initialValue:returnValue.initialValue}"></datePicker>
But this is what was rendered:
So its clear that Knockout expects component names in lower case (JSFiddle reference). The question remains is why?
I also saw similar constraints on naming of components in React, where you have to start the name with capital character only.
The key is here in the documentation:
By default, Knockout assumes that your custom element tag names correspond exactly to the names of components registered using ko.components.register. This convention-over-configuration strategy is ideal for most applications.
If you want to have different custom element tag names, you can override getComponentNameForNode to control this. For example,
ko.components.getComponentNameForNode = function(node) {
var tagNameLower = node.tagName && node.tagName.toLowerCase();
if (ko.components.isRegistered(tagNameLower)) {
// If the element's name exactly matches a preregistered
// component, use that component
return tagNameLower;
} else if (tagNameLower === "special-element") {
// For the element <special-element>, use the component
// "MySpecialComponent" (whether or not it was preregistered)
return "MySpecialComponent";
} else {
// Treat anything else as not representing a component
return null;
}
}
You can use this technique if, for example, you want to control which subset of registered components may be used as custom elements.
In fact, the documentation is not entirely up to par with the code, because it claims "correspond exactly", whereas in fact in the code only the tag name is lowercased, and the registration isn't.
In any case, because browsers return tagName in upper case, the above code will only work if your registration is in lowercase.
So, you can either use datePicker camelCased in your view and register it lower case:
<datePicker></datePicker>
ko.components.register("datepicker",{
viewModel: function() { },
template: '<strong>DatePicker stub is showing!</strong>'
});
$(function() { ko.applyBindings({}); });
Or you can monkey patch the register function:
<datePicker></datePicker>
var originalFn = ko.components.register;
ko.components.register = function(name, options) {
originalFn(name.toLowerCase(), options);
};
ko.components.register("datePicker",{
viewModel: function() { },
template: '<strong>DatePicker stub is showing!</strong>'
});
$(function() { ko.applyBindings({}); });
Though I'm not sure how stable and cross-browser compatible that would be.
In any case that shouldn't matter much, since you merely asked "why" this was like it was.
My suggestion: stick to the docs' style of dash-cased-element-names.
PS. The screenshot you posted is how the debugger rendered your elements, not how Knockout or the browser would have it. In fact, most likely if you use "view source" you'll get a camelCased element name; to be entirely sure you could inspect the raw body of the request.
Its not just the case with knockoutjs or react components, if you are writing xHTML then according to the specification tag names and attributes must be in lower case. (Ref: http://www.w3.org/TR/xhtml1/#h-4.2).
In most browsers, although the rendered html will have case insensitive tags (View Source), but the DOM built by browser(debugger tools, inspect element) will usually have all lowercase for tag names and attributes.
Although there is no wrong with case insensitive tags and attribute names, the web developers have, IMO, just adopted the convention of using lowercase names for XHTML or HTML.