Tutorial: Product Gallery

We will build a product gallery page together during this tutorial. This tutorial does not assume any existing Lynx knowledge. The techniques you'll learn in the tutorial are fundamental to building any Lynx pages and applications.

NOTE

This tutorial is designed for people who prefer to learn by doing and want to quickly try making something tangible. If you prefer learning each concept step by step, start with Describing the UI.

What are we building?

Let's first have a look at the result! To see the page live, download and install LynxExplorer on your device, then scan the generated QR code below.

Setup for the tutorial

Check out our detailed quick start doc that will guide you through creating a new Lynx project.

You may notice that the project is using TypeScript. Although Lynx and ReactLynx support both TypeScript and plain JavaScript, we recommend TypeScript for a better development experience, provided by static type checking and better editor IntelliSense.

Adding Styles

Since the focus of this tutorial is not on how to style your UI, you may just save some time and directly copy the below index.css file:

index.css
.gallery-wrapper {
  height: 100vh;
  background-color: black;
}

.single-card {
  display: flex;
  align-items: center;
  justify-content: center;
}

.scrollbar {
  position: absolute;
  right: 7px;
  z-index: 1000;
  width: 4px;
  background: linear-gradient(to bottom, #ff6448, #ccddff, #3deae7);
  border-radius: 5px;
  overflow: hidden;
  box-shadow:
    0px 0px 4px 1px rgba(12, 205, 223, 0.4),
    0px 0px 16px 5px rgba(12, 205, 223, 0.5);
}

.scrollbar-effect {
  width: 100%;
  height: 80%;
}

.glow {
  background-color: #333;
  border-radius: 4px;
  background: linear-gradient(
    45deg,
    rgba(255, 255, 255, 0) 20%,
    rgba(255, 255, 255, 0.8) 50%,
    rgba(255, 255, 255, 0) 80%
  );
  animation: flow 3s linear infinite;
}

@keyframes flow {
  0% {
    transform: translateY(-100%);
  }
  100% {
    transform: translateY(100%);
  }
}

.list {
  width: 100vw;
  padding-bottom: 20px;
  padding-left: 20px;
  padding-right: 20px;
  height: calc(100% - 48px);
  list-main-axis-gap: 10px;
  list-cross-axis-gap: 10px;
}

.picture-wrapper {
  border-radius: 10px;
  overflow: hidden;
  width: 100%;
}

.like-icon {
  position: absolute;
  display: grid;
  justify-items: center;
  align-items: center;
  top: 0px;
  right: 0px;
  width: 48px;
  height: 48px;
}

.heart-love {
  width: 16px;
  height: 16px;
}

.circle {
  position: absolute;
  top: calc(50% - 8px);
  left: calc(50% - 8px);
  height: 16px;
  width: 16px;
  border: 2px solid red;
  border-radius: 50%;
  transform: scale(0);
  opacity: 1;
  animation: ripple 1s 1 ease-out;
}

.circleAfter {
  animation-delay: 0.5s;
}

@keyframes ripple {
  0% {
    transform: scale(1);
    opacity: 1;
  }
  100% {
    transform: scale(2);
    opacity: 0;
  }
}

and import it as a global styles:

import './index.css';

This make sure your UI look great when you are following this tutorial.

Styling variations in Lynx

Lynx supports a wide variaties of styling features, including global styles, CSS Modules, inline styles, Sass, CSS variables, and more! Please refer to Rspeedy - Styling page for how to pick your best styling configurations.

Your First Component: An Image Card

Now, let's start by creating the first image card, which will be the main part of this page.

Great, you can now see the image card displayed. Here, we use the <image> element to display your image. You only need to give it a width and height (or specify the aspectRatio property as shown here), and it will automatically resize to fit the specified dimensions. This component can receive a picture property, allowing you to change the image it displays. In fact, all components can receive external inputs like this, giving you control over them.

The src Attribute of Images

The Lynx <image> element can accept a local relative path as the src attribute to render an image, which is the most important attribute of the <image> element. All images in this page are sourced locally, and these paths need to be imported before use.

However, if your images are stored online, you can easily replace them with web image addresses by changing the value of the src attribute to the corresponding web image link.

Adding interactivity: Like an Image Card

We can add a small white heart in the upper right corner and make it the like button for the image card. Here, we implement a small component called LikeIcon:

We want each card to know whether it has been liked, so we added isLiked, which is its internal data. It can use this internal data to save your changes.

LikeIcon.tsx
1...
2  const [isLiked, setIsLiked] = useState(false);
3...

Then we add the bindtap event to <image>, so that when the user clicks the heart, it triggers this event and changes the state of isLiked:

LikeIcon.tsx
1...
2  const onTap = () => {
3    setIsLiked(true);
4  }
5  return (
6      ...
7      <image bindtap={onTap}/>
8  )
9...
What is "bindtap"?

If you come from a web development background, you might be more familiar with naming conventions like onclick (HTML attribute) or onClick (in the React community). Lynx follows a different convention: due to the static nature of its architecture, it uses bind* and catch*. Learn more on the Event Handling page.

Finally, we use isLiked to control the like effect. Because isLiked is a state, LikeIcon will respond to its changes, turning into a red like icon, and the <view> used to render the animation effect will be conditionally rendered:

LikeIcon.tsx
...
  return
    ...
      {isLiked && <view className="circle" />}
      {isLiked && <view className="circle circleAfter" />}
      <image src={isLiked ? redHeart : whiteHeart} />
...

To give this like a better visual interaction effect, we added animations, which are all in index.scss. You can also learn more about animations in the Animation section. Then replace it with your preferred style!

Displaying More Images with <list>

To show all your beautiful images, you may need help from <list>. This way, you will get a scrollable page that displays a large number of similar images:

Special child elements of list

Each child component of <list> needs to be <list-item>, and you must specify a unique and non-repeating key and item-key attribute, otherwise it may not render correctly.

Of course, we also provide other scrolling elements, such as <scroll-view>, to achieve similar effects. Here, we use a waterfall layout as the child node layout option. <list> also accepts other layout types, which you can refer to in list.

INFO

You can refer to this Scrolling documentation to learn more about scrolling and scrolling elements.

Auto-Scrolling via Element Methods

If you want to create a desktop photo wall, you need to add an auto-scroll feature to this page. Your images will be slowly and automatically scrolled, allowing you to easily see more images:

We use the useEffect hook to call the autoScroll method.

Gallery.tsx
useEffect(() => {
  listRef.current
    ?.invoke({
      method: 'autoScroll',
      params: {
        rate: '60',
        start: true,
      },
    })
    .exec();
}, []);
What is "invoke"?

In Lynx, all native elements have a set of "methods" that can be called via their ref. Unlike on the web, this call is asynchronous, similar to message passing. You need to use invoke with the method name method and parameters param to call them.

How about a Custom Scrollbar?

Like most apps, we can add a scrollbar to this page to indicate how many images are left to be displayed. But we can do more! For example, we can replace the default progress bar of <list> with our preferred style:

Similar to the bindtap event used to add the like functionality, we add the bindscroll event to <list>, which will be triggered when the <list> element scrolls.

Gallery.tsx
1...
2const onScroll = (event: ScrollEvent) => {
3  scrollbarRef.current?.adjustScrollbar(
4    event.detail.scrollTop,
5    event.detail.scrollHeight
6  );
7};
8...
9<list
10  ref={galleryRef}
11  className="list"
12  list-type="waterfall"
13  column-count={2}
14  scroll-orientation="vertical"
15  custom-list-name="list-container"
16  bindscroll={onScroll}
17>
18...

The NiceScrollbar component provides an internal method adjustScrollbar, which we call to adjust the scrollbar's position whenever the bindscroll event is triggered.

INFO

We use many React techniques in this component, such as forwardRef and useImperativeHandle for calling the adjustScrollbar method. If you are not familiar with them, you can refer to the React official documentation to better understand them.

NiceScrollbar.tsx
1...
2const adjustScrollbar = (scrollTop: number, scrollHeight: number) => {
3  const listHeight = lynx.__globalProps.screenHeight - 48;
4  const scrollbarHeight = listHeight * (listHeight / scrollHeight);
5  const scrollbarTop = listHeight * (scrollTop / scrollHeight);
6  setScrollbarHeight(scrollbarHeight);
7  setScrollbarTop(scrollbarTop);
8};
9...
__globalProps

We use globalProps in this method, where you can use screenHeight and screenWidth to get the screen height and width.

list-item's estimated-main-axis-size-px

You may have noticed this attribute estimated-main-axis-size-px. This attribute can estimate the size of elements on the main axis when they are not yet rendered in <list>. This is very useful when we add a scrollbar, as we need to know how long the scrollbar needs to be to cover all elements.

Of course, <list> also supports automatic layout. You can remove this attribute and see the effect—your scrollbar will automatically adjust its length as the elements change from preset height to actual height.

src/AddNiceScrollbar/Gallery.tsx
1...
2  <list>
3    {pictureData.map((picture: Picture, index: number) => (
4      <list-item
5        estimated-main-axis-size-px={calculateEstimatedSize(
6          picture.width,
7          picture.height
8        )}
9        item-key={"" + index}
10        key={"" + index}
11      >
12        <LikeImageCard picture={picture} />
13      </list-item>
14    ))}
15  </list>
16...

We provide a utility method to estimate the size of the image on the main axis based on the current <list> layout information and the image dimensions:

src/utils.tsx
export const calculateEstimatedSize = (
  pictureWidth: number,
  pictureHeight: number,
) => {
  // Fixed styles of the gallery
  const galleryPadding = 20;
  const galleryMainAxisGap = 10;
  const gallerySpanCount = 2;
  const galleryWidth = lynx.__globalProps.screenWidth;
  // Calculate the width of each ImageCard and return the relative height of the it.
  const itemWidth =
    (galleryWidth - galleryPadding * 2 - galleryMainAxisGap) / gallerySpanCount;
  return (itemWidth / pictureWidth) * pictureHeight;
};

At this point, we have a complete page! But you may have noticed that the scrollbar we added still lags a bit during scrolling, not as responsive as it could be. This is because our adjustments are still happening on the background thread, not the main thread that responds to touch scrolling.

What are the background thread and main thread?

The biggest feature of Lynx is its dual-thread architecture. You can find a more detailed introduction in JavaScript Runtime.

A More Responsive Scrollbar

To optimize the performance of the scrollbar, we need to introduce Main Thread Script (MTS) to handle events on the main thread, migrating the adjustments we made in the previous step for the scrollbar's height and position from the background thread to the main thread.

To let you see the comparison more clearly, we keep both scrollbars:

Now you should be able to see that the scrollbar on the left, controlled with main thread scripting, is smoother and more responsive compared to the scrollbar on the right that we implemented earlier. If you encounter issues in other UIs where updates need to happen immediately, try this method.

We also provide another tutorial, guiding you through a deep dive into implementing a highly responsive swiper in Tutorial:Product Detail.

Wrapping Up

We remove the redundant scrollbar used for comparison, and our Gallery is now complete! Let's take a look at the final result:

Configurations! You have successfully created a product gallery page! 🎉 Throughout this tutorial, you’ve covered the basics of writing interactive UIs on the Lynx platform and some of the differences between using it on the Web.

Except as otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License, and code samples are licensed under the Apache License 2.0.