Catch args.data in Component on Tapped/Clicked

Hello,

I have a question about catching args.data inside Component on Tapped/Clicked.

Background:

I want to show a list of news from Observable in NewsItem component. The NewsItem component compose of title, content, and share button. The NewsItem doesn’t have any properties.

If I click on the news wrapper then it executes clickWrapper function, and if I click on share news then it will execute shareNews function then get the title and content.
However, when I log the output of those function, I always get an empty object.

Screenshots

Output:

Below is the output when I click the NewsItem component of “News 1”, and click the share button of “News 3”

[Viewport]: click wrapper
[Viewport]: {"title":"News Title 1","content":"This is content of news 1"}
[Viewport]: ------
[Viewport]: share news
[Viewport]: {}
[Viewport]: click wrapper
[Viewport]: {"title":"News Title 3","content":"This is content of news 3"}
[Viewport]: ------

Question:

How can I get the args.data of the component when I click the share button?

Is anyone know how to solve this problem?

My Files

MainView.ux

<App>
	<JavaScript>
		var Observable=require('FuseJS/Observable');
	    var news = Observable([]);
		news.replaceAll([
	        { title: 'News Title 1', content: 'This is content of news 1' },
	        { title: 'News Title 2' , content: 'This is content of news 2' },
	        { title: 'News Title 3'  , content: 'This is content of news 3' },
	        { title: 'News Title 4', content: 'This is content of news 4' }]);

	    module.exports = {
	    	news: news
	    };
	</JavaScript>
	<StackPanel Margin="0,50">
		<Each Items="{news}">
			<NewsItem />  
		</Each>
	</StackPanel>
</App>

NewsItem.ux

<Panel ux:Class="NewsItem" Color="#eaeaea" Margin="10,5">
  <JavaScript>
    clickWrapper = (args) => {
      console.log('click wrapper', JSON.stringify(args.data));
      console.log('------');
    }
    openNews = (args) => {
      console.log('open news', JSON.stringify(args.data));
    }
    shareNews = (args) => {
      console.log('share news', JSON.stringify(args.data));
    }

    module.exports = {
      clickWrapper,
      openNews,
      shareNews
    }
  </JavaScript>
  <Tapped>
    <Callback Handler="{clickWrapper}"></Callback>
  </Tapped>
  <StackPanel Padding="10">
    <StackPanel>
      <Tapped>
        <Callback Handler="{openNews}"></Callback>
      </Tapped>
      <Text Value="{title}" />	  
      <Text Value="{content}" FontSize="14"  />	
    </StackPanel>
    <Text TextAlignment="Right" FontSize="12" Color="Red" Value="Share">
      <Tapped>
        <Callback Handler="{shareNews}"></Callback>
      </Tapped>
    </Text>  
  </StackPanel>
</Panel>

So the root cause of the problem you’re describing stems from a simple fact: when you introduce a JavaScript viewmodel in the component, it overwrites whatever other implicit data context was there.

That results in an empty object in the callback, since your new viewmodel doesn’t really export any data - it only contains those callbacks you’ve defined.

There are several ways you can work around this issue.

Most obvious one: put the callbacks in the parent data context, right next to where you have the news list. You can still do <Callback Handler="{...}" /> from inside the component, since Fuse looks up the tree to find the nearest place where that callback is implemented. Don’t forget to remove <JavaScript> tags from the component! Something like this:

<App>
    <JavaScript>
        var Observable=require('FuseJS/Observable');
        var news = Observable(
            { title: 'News Title 1', content: 'This is content of news 1' },
            { title: 'News Title 2' , content: 'This is content of news 2' },
            { title: 'News Title 3'  , content: 'This is content of news 3' },
            { title: 'News Title 4', content: 'This is content of news 4' }
        );
        function globalCallback(args) {
            console.log("global: " + JSON.stringify(args));
        }

        module.exports = {
            news: news,
            globalCallback: globalCallback
        };
    </JavaScript>

    <StackPanel Margin="0,50">
        <Each Items="{news}">
            <NewsItem Height="56" />
        </Each>
    </StackPanel>

    <Panel ux:Class="NewsItem" Color="#eaeaea" Margin="10,5">
      <Clicked>
        <Callback Handler="{globalCallback}" />
      </Clicked>
      <Text Value="{title}" Alignment="Center" />
    </Panel>

</App>

Second, you could use ux:Property to pass the params as properties to the component, and have a local JavaScript viewmodel that module.exports a local version of the data, like so:

<App>
    <JavaScript>
        var Observable=require('FuseJS/Observable');
        var news = Observable(
            { title: 'News Title 1', content: 'This is content of news 1' },
            { title: 'News Title 2' , content: 'This is content of news 2' },
            { title: 'News Title 3'  , content: 'This is content of news 3' },
            { title: 'News Title 4', content: 'This is content of news 4' }
        );

        module.exports = {
            news: news
        };
    </JavaScript>

    <StackPanel Margin="0,50">
        <Each Items="{news}">
            <NewsItem Height="56" Title="{title}" Content="{content}" />
        </Each>
    </StackPanel>

    <Panel ux:Class="NewsItem" Color="#eaeaea" Margin="10,5">
      <string ux:Property="Title" />
      <string ux:Property="Content" />
      <JavaScript>
      function localCallback(args) {
        console.log("local: " + JSON.stringify(args));
      }
      module.exports = {
        localCallback: localCallback,
        localTitle: this.Title,
        localContent: this.Content
      };
      </JavaScript>
      <Clicked>
        <Callback Handler="{localCallback}" />
      </Clicked>
      <Text Value="{localTitle}" Alignment="Center" />
    </Panel>

</App>

And third option would be to look into using UserEvents.

Hope this helps!

So the root cause of the problem you’re describing stems from a simple fact: when you introduce a JavaScript viewmodel in the component, it overwrites whatever other implicit data context was there.

That results in an empty object in the callback, since your new viewmodel doesn’t really export any data - it only contains those callbacks you’ve defined.

Whoa! I just know it. Thank you @Uldis for the enlightenment :smiley:

Since I’m familiar with vuejs & angular, and my NewsItem component will be used in many pages with the same behaviour defined in the javascript (going to another route, save or remove from bookmark, share to social media, etc), so I will stick with solution no.2 passing the object as properties.


MainView.ux

<App>
	<JavaScript>
		var Observable=require('FuseJS/Observable');
	    var news = Observable([]);
		news.replaceAll([
	        { title: 'News Title 1', content: 'This is content of news 1' },
	        { title: 'News Title 2' , content: 'This is content of news 2' },
	        { title: 'News Title 3'  , content: 'This is content of news 3' },
	        { title: 'News Title 4', content: 'This is content of news 4' }]);

	    module.exports = {
	    	news: news
	    };
	</JavaScript>
	<StackPanel Margin="0,50">
		<Each Items="{news}">
			<NewsItem Post="{}" />  
		</Each>
	</StackPanel>
</App>

NewsItem.ux

<Panel ux:Class="NewsItem" Color="#eaeaea" Margin="10,5">
  <object ux:Property="Post" />
  <JavaScript>
    var Observable = require('FuseJS/Observable');
    // initialize empty value first
    var post = Observable({
      title: '',
      content: '',
    });
    post.value = this.Post.value;

    openNews = (args) => {
      console.log('open news', JSON.stringify(post.value));
    }
    shareNews = (args) => {
      console.log('share news', JSON.stringify(post.value));
    }

    module.exports = {
      post,
      openNews,
      shareNews
    }
  </JavaScript>
  
  <StackPanel Padding="10">
    <StackPanel>
      <Tapped>
        <Callback Handler="{openNews}"></Callback>
      </Tapped>
      <Text Value="{post.title}" />	  
      <Text Value="{post.content}" FontSize="14"  />	
    </StackPanel>
    <Text TextAlignment="Right" FontSize="12" Color="Red" Value="Share">
      <Tapped>
        <Callback Handler="{shareNews}"></Callback>
      </Tapped>
    </Text>  
  </StackPanel>
</Panel>

Output when click on content of News 2 and click on share button on News 3

[Viewport]: open news
[Viewport]: {"title":"News Title 2","content":"This is content of news 2"}
[Viewport]: share news
[Viewport]: {"title":"News Title 3","content":"This is content of news 3"}

Regards.

Great!

Only note that the properties are implicit derived Observables, so they get populated async. A general pattern we suggest to use inside components looks something like this:

var localTitle = this.Title.map(function(x) {
    return x;
});

You can, of course, use any other reactive operator.

Don’t access these observables imperatively. Doing console.log(this.Title.value) will result in an empty value, because it simply hasn’t arrived yet.