📜 ⬆️ ⬇️

Does the designer have a new idea? What could be easier

Hi, habrovchanin! Designers are ideological people, and customers with their business requirements, all the more.

Imagine that you made your best UIkit in the world on the coolest% to insert your% JS framework. It would seem that there is everything that the project needs. Now you will be able to drink coffee, and all new tasks to close the components on the page. Even better, if you found such a UIkit in the garbage in the vastness of NPM and it perfectly matches the current UX / UI and its needs. Fantasy!

And really ... who am I kidding? Your happiness will most likely be short lived. After all, when the designer comes running with the Talmud of new UI-solutions for the next page or “special project”, something will go wrong anyway.

At this point, the developer is faced with the question "DRY or not DRY" ? Is it worth it to customize existing components? Yes, so as not to grab regreshn on existing cases. Or, act on the principle of "work - do not touch" and write new components from scratch. At the same time, inflating UIkit and complicating support.

If you, like many, have been in this situation, look under the cat!



Despite the expanded introduction, the idea of ​​writing this article came to me after reading one of the threads of comments on Habré. There, the guys are not really sprayed about how to customize the button component on React. Well, after I watched a couple of similar holivars in Telegram, the need to write about it was finally strengthened.

To begin with, let's try to imagine what kind of “customization” we may need to apply to the component.

Styles



First of all, it is customization of component styles. A trivial example - the button is gray, but you need a blue one. Or a button without rounded corners and suddenly they are needed. Based on the holivars I read, I concluded that there are about 3 approaches for this:

1. Global Styles


Use all the power of global CSS styles, pretty much forwarded ! Important , to outside, globally, try to override the styles of the original component. The decision, to put it mildly, is controversial and too rectilinear. In addition, this option is simply not always possible, and at the same time it desperately breaks any encapsulation of styles. Unless of course it is used in your components.

2. Transfer of classes (styles) from a higher context


Also quite a controversial decision. It turns out, we create a special prop , for example, for example, we will call its classes and immerse the necessary classes directly into the component from the top.

<Button classes="btn-red btn-rounded" /> 

This approach will naturally work only if the component supports applying styles to its content in this way. In addition, if a component is slightly more complex and consists of a nested structure of HTML elements, then obviously applying styles to all will be problematic. So, they will be applied to the root element of the component, and then with the help of CSS rules in some way spread further. Sadly.

3. Setting up the component through props


It looks like the most sensible, but at the same time the least flexible solution. Simply put, we expect that the author of the component is some kind of genius and has thought through all the options beforehand. That is all that we may need and determined all the necessary props for all the desired results:

 <Button bgColor="red" rounded={true} /> 

It doesn't sound very believable, does it? Perhaps.

Behavior




It's still more ambiguous. First of all, because the difficulties in customizing the behavior of the component come from the task. The more complex the component and the logic embedded in it, and the more complex the change we want to make, the more difficult it is to make this change. Some kind of tautology turned out ... In short, you understand! ;-)

However, even here, there is a set of tools that either help us customize a component or not. Since we are talking about the component approach, I would highlight the following useful tools:

1. Convenient work with props


First of all, you need to be able to mimic the set of component propses without the need to re-describe this set and conveniently proxy them further.

Further, if we try to add some behavior to the component, then, most likely, we will need to use an additional set of props that the original component does not need. Therefore, it is good to be able to cut off part of the props and transfer only what is necessary to the original component. At the same time keeping all properties synchronized.

The flip side is when we want to implement a particular case of a component's behavior. Somehow fix part of his state on a specific task.

2. Tracking the life cycle and component events


In other words, everything that happens inside a component should not be a completely closed book. Otherwise it will really complicate the customization of his behavior.

I do not mean a violation of encapsulation and uncontrolled intervention inside. The component should be managed through its public api (usually these are props and / or methods). But to be able to somehow “find out” about what is happening inside and track the change in its state is still necessary.

3. Imperative control method


We assume that I did not tell you that. And yet, sometimes, it is not bad to be able to get an instance component and imperatively “pull the strings”. It is better to avoid this, but in particularly complex cases, it can not be avoided.

Ok, sort of dealt with theory. By and large, everything is obvious, but not everything is clear. Therefore it is worth considering at least some real case.

Case




Above, I mentioned that the idea of ​​writing an article arose from the holivar about customizing the button. Therefore, I thought that it would be symbolic to solve such a case. It would be too easy to change the color or round the corners, so I tried to come up with a slightly more complicated case.

Imagine that we have a certain component of the basic button, which is used in the jilion of the application. In addition, it implements some basic behavior for all application buttons, as well as a set of basic, encapsulated styles that, from time to time, are synchronized with UI guides and all that.

Further, it becomes necessary to have an additional component for the submit button on the server (submit button), which, in addition to style changes, requires the implementation of additional behavior. For example, it may be drawing the progress of sending, as well as a visual representation of the result of this action - successfully or not successfully.

Something like this might look like:



It is not difficult to guess that the basic button is on the left, and the send button on the right is in the state of successful completion of the request. Well, if the case is clear - let's start the implementation!

Decision


I never managed to figure out what exactly caused holivar in the decision on React. Apparently there is not so simple. Therefore, I will not tempt fate and use the more familiar tool for me - SvelteJS - a disappearing framework of the new generation , which is almost ideal for solving such problems .

We will immediately agree, we will not interfere in any way with the code of the basic button. We assume that it is generally not written by us and its code is closed for corrections. In this case, the base button component will look something like this:

Button.html

 <button {type} {name} {value} {disabled} {autofocus} on:click > <slot></slot> </button> <script> export default { data() { return { type: 'button', disabled: false, autofocus: false, value: '', name: '' }; } }; </script> <style> /* scoped styles */ </style> 

And used in this way:

 <Button on:click="cancel()">Cancel</Button> 

Note that the button component is really very basic. It contains absolutely no auxiliary elements or props that could help in the implementation of the advanced version of the component. This component does not even support the transfer of styles of props or at least some built-in customization, and all styles are strictly isolated and do not flow out.

Creating another component based on this component, with extended functionality, and without making any changes, may seem like a simple task. But not when you use Svelte .

Now let's define what the submit button should be able to:

  1. First of all, the frame and button text should be green. When hovering, the background should also be green, instead of dark gray.
  2. Further, when you press the button, it should “slam” into the round progress indicator.
  3. Upon completion of the process (which is controlled from the outside), it is necessary that the button status can be changed to successful (success) or not successful (error). At the same time, the button from the indicator should turn into either a green badge with a daw or a red badge with a cross.
  4. It is also necessary to be able to set the time after which the corresponding badge will again turn into a button in its original state (idle).
  5. And of course, you need to do all this on top of the basic button with the preservation and application of all styles and props from there.

Fuh, not an easy task. Let's first create a new component and wrap the base button with it:

SubmitButton.html

 <Button> <slot></slot> </Button> <script> import Button from './Button.html'; export default { components: { Button } }; </script> 

While this is exactly the same button, only worse - she does not even know how to proxy proxy. It does not matter, back to this later.

We stylize


In the meantime, let's think about how we can stylize a new button, namely, change colors, according to the task. Unfortunately, it seems we can not use any of the approaches described above.

Since the styles are isolated inside the button, there may be problems with global styles. Throwing styles inside also does not work out - the basic button simply does not support this feature. As well as customization of appearance using props. In addition, we would like all styles written for the new button to also be encapsulated inside this button and not leak out.

The solution is incredibly simple, but only if you are already using Svelte . So, just write the styles for the new button:

 <div class="submit"> <Button> <slot></slot> </Button> </div> ... <style> .submit :global(button) { border: 2px solid #1ECD97; color: #1ECD97; } .submit :global(button:hover) { background-color: #1ECD97; color: #fff; } </style> 

One of the Svelte keynotes is that simple things should be solved easily. The special modifier : global in this version will generate CSS in such a way that only the buttons inside the block with the submit class that are in this component will receive the specified styles.

Even if in any other place of the application, a markup of the same kind suddenly appears:

 <div class="submit"> <button>Button</button> </div> 

styles from the SubmitButton component will not “flow” there in any way.

Using this method, Svelte makes it easier to easily customize the styles of nested components, while preserving the encapsulation of the styles of both components.

We throw props and fix the behavior


Well, we dealt with styling almost instantly and without any additional props and transferring CSS classes directly. Now we need to proxy all props of the Button component through the new component. In this case, I would not like to describe them again. However, for a start, let's decide which properties a new component will have.

Judging by the task, SubmitButton should track the state, as well as give the opportunity to specify the time delay between the automatic change of the successful / erroneous state to the initial one:

 <script> ... export default { ... data() { return { delay: 1500, status: 'idle' // loading, success, error }; } }; </script> 

So, our new button will have 4 states: rest, load, success or error. In addition, by default, the last 2 states will automatically change to a resting state after 1.5 seconds.

In order to throw all the passed props into the Button component, but at the same time cutting off the status and delay that are obviously invalid for it, we will write a special calculated property. After that, we simply use the spread operator to “spread out” the remaining props on the nested component. In addition, since we are doing exactly the send button, we need to fix the button type so that it cannot be changed from the outside:

 <div class="submit"> <Button {...attrs} type="submit"> <slot></slot> </Button> </div> <script> ... export default { ... computed: { attrs: data => { const { delay, status, ...attrs } = data; return attrs; } }, }; </script> 

Pretty simple and elegant.

As a result, we got a fully working version of the basic button with modified styles. It's time to start implementing a new button behavior.

We change and monitor the state


So, when you click on the SubmitButton button , we must not only throw the event out, so that the custom code can process it (as was done in Button ), but also implement additional business logic — set the loading status. To do this, simply intercept the event from the base button into your own handler, do what you need and send it further:

 <div class="submit"> <Button {...attrs} type="submit" on:click="click(event)"> <slot></slot> </Button> </div> <script> ... export default { ... methods: { click(e) { this.set({ status: 'loading' }); this.fire('click', e); } }, }; </script> 

Further, the parent component of this button, which controls the data sending process itself, can set the corresponding send status ( success / error ) through the props. At the same time, the button should track such a status change, and after a specified time, automatically change the state to idle . To do this, use the onupdate hook on the life-cycle :

 <script> ... export default { ... onupdate({ current: { status, delay }, changed }) { if (changed.status && ['success', 'error'].includes(status)) { setTimeout(() => this.set({ status: 'idle' }), delay); } }, }; </script> 

Finishing touches


There are still 2 points that are not obvious from the task and arise during implementation. First, in order for the animation of the metamorphosis of a button to be smooth, you will have to change the style of the button itself instead of some other element. For this we can use the same : global , so there are no problems here. But besides, it is necessary that the markup inside the button is hidden in all statuses except idle .

It is worth mentioning separately that the markup inside the button can be any and it thrusts into the original component of the basic button through nested slots. However, although it sounds threatening, the solution is more than primitive - you just need to wrap the slot, inside the new component into an additional element and apply the necessary styles to it:

 <div class="submit"> <Button {...attrs} type="submit" on:click="click(event)"> <span><slot></slot></span> </Button> </div> ... <style> ... .submit span { transition: opacity 0.3s 0.1s; } .submit.loading span, .submit.success span, .submit.error span { opacity: 0; } ... </style> 

In addition, since the button is not hidden from the page, but morphs along with the statuses, it would be good to disable it at the time of sending. In other words, if the sending button was set to the disabled property through props, or if status is not idle , you must disable the button. To solve this problem, we will write another small calculated property isDisabled and apply it to the nested component:

 <div class="submit"> <Button {...attrs} type="submit" disabled={isDisabled}> <span><slot></slot></span> </Button> </div> <script> ... export default { ... computed: { ... isDisabled: ({ status, disabled }) => disabled || status !== 'idle' }, }; </script> 

Everything would be fine, but a style is registered in the basic button, which makes it translucent in the disabled state, and we don’t need it if the button is only temporarily disabled due to the change of statuses. It all comes to the rescue : global :

  .submit.loading :global(button[disabled]), .submit.success :global(button[disabled]), .submit.error :global(button[disabled]) { opacity: 1; } 

That's all! New button is beautiful and ready to go!



I deliberately omit the details of the implementation of animations and all of this. Not only because it is not directly related to the topic of the article, but also because in this part the demo did not turn out the way we would like. I did not complicate the task and implement a completely ready solution for such a button and rather stupidly ported the example found on the Internet.

Therefore, I do not advise using this implementation in the work. Remember, this is just a demo example for this article.

Interactive demo and full example code

If you liked the article and wanted to learn more about Svelte , read other articles . For example, "How to make a search for users on GitHub without React + RxJS 6 + Recompose" . Listen to the pre-New Year podcast RadioJS # 54 , where I talked in some detail on what Svelte is , how it “disappears” and why it is not “yet another js framework”.

Look into the Russian telegram channel SvelteJS . There are already more than two hundred of us and we will be happy for you!

P / S

Suddenly, UI guidelines have changed. Now the labels in all buttons of the application should be in upper case. However, such a turn of events is not terrible for us. Add a text-transform: uppercase; in the style of the basic button and continue to drink coffee.

Have a good working day and you!

Source: https://habr.com/ru/post/438834/